2026-06-22 21:57:32 +02:00
|
|
|
import { create } from 'zustand'
|
2026-06-25 13:28:40 +02:00
|
|
|
import type { PeerInfo, NetworkInfo, ChatMessage, FileEntry, IpcMessage, PeerConnState, CandidateType } from '../types'
|
|
|
|
|
|
|
|
|
|
export interface PeerStatus {
|
|
|
|
|
connState?: PeerConnState
|
|
|
|
|
candidateType?: CandidateType
|
|
|
|
|
remoteAddress?: string
|
|
|
|
|
lastSeen?: number
|
|
|
|
|
}
|
2026-06-22 21:57:32 +02:00
|
|
|
import { DaemonAdapter } from '../adapter/daemon'
|
2026-06-23 00:06:41 +02:00
|
|
|
import { BrowserAdapter } from '../adapter/browser'
|
|
|
|
|
|
|
|
|
|
type AnyAdapter = DaemonAdapter | BrowserAdapter
|
2026-06-22 21:57:32 +02:00
|
|
|
|
|
|
|
|
interface WasteState {
|
|
|
|
|
// connection
|
2026-06-23 00:06:41 +02:00
|
|
|
adapter: AnyAdapter | null
|
|
|
|
|
adapterMode: 'daemon' | 'browser' | null
|
2026-06-22 21:57:32 +02:00
|
|
|
daemonStatus: 'disconnected' | 'connecting' | 'connected'
|
|
|
|
|
|
|
|
|
|
// identity
|
2026-06-22 23:22:32 +02:00
|
|
|
masterAlias: string | null
|
|
|
|
|
masterId: string | null
|
2026-06-22 21:57:32 +02:00
|
|
|
localPeer: PeerInfo | null
|
|
|
|
|
|
|
|
|
|
// networks
|
|
|
|
|
networks: NetworkInfo[]
|
|
|
|
|
activeNetworkId: string | null
|
|
|
|
|
|
|
|
|
|
// peers
|
|
|
|
|
connectedPeers: PeerInfo[]
|
|
|
|
|
|
|
|
|
|
// chat — keyed by room
|
|
|
|
|
messages: Record<string, ChatMessage[]>
|
|
|
|
|
activeRoom: string
|
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>
2026-06-25 20:49:15 +02:00
|
|
|
// user-created rooms, keyed by networkId
|
|
|
|
|
customRooms: Record<string, string[]>
|
2026-06-22 21:57:32 +02:00
|
|
|
|
|
|
|
|
// file listings — keyed by peer id
|
|
|
|
|
fileLists: Record<string, FileEntry[]>
|
|
|
|
|
|
2026-06-25 13:28:40 +02:00
|
|
|
// per-peer connection status (browser mode) and last-seen timestamps
|
|
|
|
|
peerStatus: Record<string, PeerStatus>
|
|
|
|
|
|
2026-06-22 23:22:32 +02:00
|
|
|
// identity backup export result (cleared when consumed)
|
|
|
|
|
exportedBackup: string | null
|
|
|
|
|
|
Add peer-to-peer file transfer in browser mode
Implements both pull (browse & download from a shared folder) and push
(send a file directly to a peer) using the yaw2 file protocol over WebRTC
DataChannels.
- PeerConn: handle browse/files/get/file-offer/file-accept/file-done
control messages; stream files in 64KB chunks over f:xid binary
DataChannels; auto-accept incoming offers (push and pull)
- PeerConn.sendFilePush(): initiate an outbound file transfer without
requiring the peer to browse first
- BrowserAdapter: sharedFiles map, setSharedFiles(), requestBrowse(),
requestGet(), sendFileTo() — propagates share map to all active peers
- Store: activeFilePeer, setActiveFilePeer, setSharedFiles, browseFiles,
sendFileTo; file_complete handler triggers browser download automatically
- FileBrowser component: third-column panel listing a peer's shared files
with name, size, and a download button
- FolderPicker component: webkitdirectory input in the sidebar (browser
mode only) for selecting a local folder to share
- Sidebar: 📎 send-file button on peer rows (hover) with inline file picker
- App.tsx: use browser mode when WASTE_CONFIG.signalURL is set, even on
localhost — allows testing browser mode via npm run dev with config.js
- deploy-daemon.sh: kill existing daemon before scp to avoid upload failure
when the binary is locked; remove duplicate kill block in restart step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 20:29:54 +02:00
|
|
|
// file browser state
|
|
|
|
|
activeFilePeer: string | null
|
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>
2026-06-25 20:49:15 +02:00
|
|
|
// 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 }>
|
Add peer-to-peer file transfer in browser mode
Implements both pull (browse & download from a shared folder) and push
(send a file directly to a peer) using the yaw2 file protocol over WebRTC
DataChannels.
- PeerConn: handle browse/files/get/file-offer/file-accept/file-done
control messages; stream files in 64KB chunks over f:xid binary
DataChannels; auto-accept incoming offers (push and pull)
- PeerConn.sendFilePush(): initiate an outbound file transfer without
requiring the peer to browse first
- BrowserAdapter: sharedFiles map, setSharedFiles(), requestBrowse(),
requestGet(), sendFileTo() — propagates share map to all active peers
- Store: activeFilePeer, setActiveFilePeer, setSharedFiles, browseFiles,
sendFileTo; file_complete handler triggers browser download automatically
- FileBrowser component: third-column panel listing a peer's shared files
with name, size, and a download button
- FolderPicker component: webkitdirectory input in the sidebar (browser
mode only) for selecting a local folder to share
- Sidebar: 📎 send-file button on peer rows (hover) with inline file picker
- App.tsx: use browser mode when WASTE_CONFIG.signalURL is set, even on
localhost — allows testing browser mode via npm run dev with config.js
- deploy-daemon.sh: kill existing daemon before scp to avoid upload failure
when the binary is locked; remove duplicate kill block in restart step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 20:29:54 +02:00
|
|
|
|
2026-06-22 21:57:32 +02:00
|
|
|
// actions
|
|
|
|
|
connect: (url: string) => void
|
2026-06-23 00:06:41 +02:00
|
|
|
connectBrowser: () => void
|
2026-06-22 21:57:32 +02:00
|
|
|
disconnect: () => void
|
|
|
|
|
send: (msg: IpcMessage) => void
|
|
|
|
|
setActiveNetwork: (id: string) => void
|
|
|
|
|
setActiveRoom: (room: string) => void
|
Add peer-to-peer file transfer in browser mode
Implements both pull (browse & download from a shared folder) and push
(send a file directly to a peer) using the yaw2 file protocol over WebRTC
DataChannels.
- PeerConn: handle browse/files/get/file-offer/file-accept/file-done
control messages; stream files in 64KB chunks over f:xid binary
DataChannels; auto-accept incoming offers (push and pull)
- PeerConn.sendFilePush(): initiate an outbound file transfer without
requiring the peer to browse first
- BrowserAdapter: sharedFiles map, setSharedFiles(), requestBrowse(),
requestGet(), sendFileTo() — propagates share map to all active peers
- Store: activeFilePeer, setActiveFilePeer, setSharedFiles, browseFiles,
sendFileTo; file_complete handler triggers browser download automatically
- FileBrowser component: third-column panel listing a peer's shared files
with name, size, and a download button
- FolderPicker component: webkitdirectory input in the sidebar (browser
mode only) for selecting a local folder to share
- Sidebar: 📎 send-file button on peer rows (hover) with inline file picker
- App.tsx: use browser mode when WASTE_CONFIG.signalURL is set, even on
localhost — allows testing browser mode via npm run dev with config.js
- deploy-daemon.sh: kill existing daemon before scp to avoid upload failure
when the binary is locked; remove duplicate kill block in restart step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 20:29:54 +02:00
|
|
|
setActiveFilePeer: (peerId: string | null) => void
|
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>
2026-06-25 20:49:15 +02:00
|
|
|
setSharedFiles: (files: Map<string, File>, networkId?: string) => void
|
Add peer-to-peer file transfer in browser mode
Implements both pull (browse & download from a shared folder) and push
(send a file directly to a peer) using the yaw2 file protocol over WebRTC
DataChannels.
- PeerConn: handle browse/files/get/file-offer/file-accept/file-done
control messages; stream files in 64KB chunks over f:xid binary
DataChannels; auto-accept incoming offers (push and pull)
- PeerConn.sendFilePush(): initiate an outbound file transfer without
requiring the peer to browse first
- BrowserAdapter: sharedFiles map, setSharedFiles(), requestBrowse(),
requestGet(), sendFileTo() — propagates share map to all active peers
- Store: activeFilePeer, setActiveFilePeer, setSharedFiles, browseFiles,
sendFileTo; file_complete handler triggers browser download automatically
- FileBrowser component: third-column panel listing a peer's shared files
with name, size, and a download button
- FolderPicker component: webkitdirectory input in the sidebar (browser
mode only) for selecting a local folder to share
- Sidebar: 📎 send-file button on peer rows (hover) with inline file picker
- App.tsx: use browser mode when WASTE_CONFIG.signalURL is set, even on
localhost — allows testing browser mode via npm run dev with config.js
- deploy-daemon.sh: kill existing daemon before scp to avoid upload failure
when the binary is locked; remove duplicate kill block in restart step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 20:29:54 +02:00
|
|
|
browseFiles: (peerId: string) => void
|
|
|
|
|
sendFileTo: (peerId: string, file: File) => void
|
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>
2026-06-25 20:49:15 +02:00
|
|
|
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
|
2026-06-26 20:16:18 +02:00
|
|
|
logout: (clearIdentity: boolean) => void
|
2026-06-22 21:57:32 +02:00
|
|
|
handleEvent: (msg: IpcMessage) => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const useWaste = create<WasteState>((set, get) => ({
|
|
|
|
|
adapter: null,
|
2026-06-23 00:06:41 +02:00
|
|
|
adapterMode: null,
|
2026-06-22 21:57:32 +02:00
|
|
|
daemonStatus: 'disconnected',
|
2026-06-22 23:22:32 +02:00
|
|
|
masterAlias: null,
|
|
|
|
|
masterId: null,
|
2026-06-22 21:57:32 +02:00
|
|
|
localPeer: null,
|
|
|
|
|
networks: [],
|
|
|
|
|
activeNetworkId: null,
|
|
|
|
|
connectedPeers: [],
|
|
|
|
|
messages: {},
|
|
|
|
|
activeRoom: 'general',
|
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>
2026-06-25 20:49:15 +02:00
|
|
|
customRooms: {},
|
2026-06-22 21:57:32 +02:00
|
|
|
fileLists: {},
|
2026-06-22 23:22:32 +02:00
|
|
|
exportedBackup: null,
|
2026-06-25 13:28:40 +02:00
|
|
|
peerStatus: {},
|
Add peer-to-peer file transfer in browser mode
Implements both pull (browse & download from a shared folder) and push
(send a file directly to a peer) using the yaw2 file protocol over WebRTC
DataChannels.
- PeerConn: handle browse/files/get/file-offer/file-accept/file-done
control messages; stream files in 64KB chunks over f:xid binary
DataChannels; auto-accept incoming offers (push and pull)
- PeerConn.sendFilePush(): initiate an outbound file transfer without
requiring the peer to browse first
- BrowserAdapter: sharedFiles map, setSharedFiles(), requestBrowse(),
requestGet(), sendFileTo() — propagates share map to all active peers
- Store: activeFilePeer, setActiveFilePeer, setSharedFiles, browseFiles,
sendFileTo; file_complete handler triggers browser download automatically
- FileBrowser component: third-column panel listing a peer's shared files
with name, size, and a download button
- FolderPicker component: webkitdirectory input in the sidebar (browser
mode only) for selecting a local folder to share
- Sidebar: 📎 send-file button on peer rows (hover) with inline file picker
- App.tsx: use browser mode when WASTE_CONFIG.signalURL is set, even on
localhost — allows testing browser mode via npm run dev with config.js
- deploy-daemon.sh: kill existing daemon before scp to avoid upload failure
when the binary is locked; remove duplicate kill block in restart step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 20:29:54 +02:00
|
|
|
activeFilePeer: null,
|
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>
2026-06-25 20:49:15 +02:00
|
|
|
sharedFilesByNetwork: {},
|
|
|
|
|
pendingOffers: {},
|
|
|
|
|
fileProgress: {},
|
2026-06-22 21:57:32 +02:00
|
|
|
|
|
|
|
|
connect(url: string) {
|
|
|
|
|
const adapter = new DaemonAdapter(url)
|
|
|
|
|
adapter.onStatusChange = (s) => set({ daemonStatus: s })
|
|
|
|
|
adapter.on((msg) => get().handleEvent(msg))
|
|
|
|
|
adapter.connect()
|
2026-06-23 00:06:41 +02:00
|
|
|
set({ adapter, adapterMode: 'daemon' })
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
connectBrowser() {
|
|
|
|
|
const adapter = new BrowserAdapter()
|
|
|
|
|
adapter.onStatusChange = (s) => set({ daemonStatus: s })
|
|
|
|
|
adapter.on((msg) => get().handleEvent(msg))
|
|
|
|
|
adapter.connect()
|
|
|
|
|
set({ adapter, adapterMode: 'browser' })
|
2026-06-22 21:57:32 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
disconnect() {
|
2026-06-23 00:06:41 +02:00
|
|
|
const a = get().adapter
|
|
|
|
|
if (a instanceof DaemonAdapter) a.disconnect()
|
|
|
|
|
else if (a instanceof BrowserAdapter) a.disconnect()
|
|
|
|
|
set({ adapter: null, adapterMode: null, daemonStatus: 'disconnected' })
|
2026-06-22 21:57:32 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
send(msg) {
|
|
|
|
|
get().adapter?.send(msg)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setActiveNetwork(id) {
|
|
|
|
|
set({ activeNetworkId: id })
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setActiveRoom(room) {
|
|
|
|
|
set({ activeRoom: room })
|
|
|
|
|
},
|
|
|
|
|
|
Add peer-to-peer file transfer in browser mode
Implements both pull (browse & download from a shared folder) and push
(send a file directly to a peer) using the yaw2 file protocol over WebRTC
DataChannels.
- PeerConn: handle browse/files/get/file-offer/file-accept/file-done
control messages; stream files in 64KB chunks over f:xid binary
DataChannels; auto-accept incoming offers (push and pull)
- PeerConn.sendFilePush(): initiate an outbound file transfer without
requiring the peer to browse first
- BrowserAdapter: sharedFiles map, setSharedFiles(), requestBrowse(),
requestGet(), sendFileTo() — propagates share map to all active peers
- Store: activeFilePeer, setActiveFilePeer, setSharedFiles, browseFiles,
sendFileTo; file_complete handler triggers browser download automatically
- FileBrowser component: third-column panel listing a peer's shared files
with name, size, and a download button
- FolderPicker component: webkitdirectory input in the sidebar (browser
mode only) for selecting a local folder to share
- Sidebar: 📎 send-file button on peer rows (hover) with inline file picker
- App.tsx: use browser mode when WASTE_CONFIG.signalURL is set, even on
localhost — allows testing browser mode via npm run dev with config.js
- deploy-daemon.sh: kill existing daemon before scp to avoid upload failure
when the binary is locked; remove duplicate kill block in restart step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 20:29:54 +02:00
|
|
|
setActiveFilePeer(peerId) {
|
|
|
|
|
set({ activeFilePeer: peerId })
|
|
|
|
|
},
|
|
|
|
|
|
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>
2026-06-25 20:49:15 +02:00
|
|
|
setSharedFiles(files, networkId) {
|
|
|
|
|
const netId = networkId ?? get().activeNetworkId ?? ''
|
|
|
|
|
if (!netId) return
|
|
|
|
|
set(s => ({ sharedFilesByNetwork: { ...s.sharedFilesByNetwork, [netId]: files } }))
|
Add peer-to-peer file transfer in browser mode
Implements both pull (browse & download from a shared folder) and push
(send a file directly to a peer) using the yaw2 file protocol over WebRTC
DataChannels.
- PeerConn: handle browse/files/get/file-offer/file-accept/file-done
control messages; stream files in 64KB chunks over f:xid binary
DataChannels; auto-accept incoming offers (push and pull)
- PeerConn.sendFilePush(): initiate an outbound file transfer without
requiring the peer to browse first
- BrowserAdapter: sharedFiles map, setSharedFiles(), requestBrowse(),
requestGet(), sendFileTo() — propagates share map to all active peers
- Store: activeFilePeer, setActiveFilePeer, setSharedFiles, browseFiles,
sendFileTo; file_complete handler triggers browser download automatically
- FileBrowser component: third-column panel listing a peer's shared files
with name, size, and a download button
- FolderPicker component: webkitdirectory input in the sidebar (browser
mode only) for selecting a local folder to share
- Sidebar: 📎 send-file button on peer rows (hover) with inline file picker
- App.tsx: use browser mode when WASTE_CONFIG.signalURL is set, even on
localhost — allows testing browser mode via npm run dev with config.js
- deploy-daemon.sh: kill existing daemon before scp to avoid upload failure
when the binary is locked; remove duplicate kill block in restart step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 20:29:54 +02:00
|
|
|
const a = get().adapter
|
|
|
|
|
if (a instanceof BrowserAdapter) a.setSharedFiles(files)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
sendFileTo(peerId, file) {
|
|
|
|
|
const a = get().adapter
|
|
|
|
|
if (a instanceof BrowserAdapter) a.sendFileTo(peerId, file)
|
|
|
|
|
},
|
|
|
|
|
|
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>
2026-06-25 20:49:15 +02:00
|
|
|
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)
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-26 20:16:18 +02:00
|
|
|
logout(clearIdentity) {
|
|
|
|
|
const a = get().adapter
|
|
|
|
|
if (a instanceof DaemonAdapter) a.disconnect()
|
|
|
|
|
else if (a instanceof BrowserAdapter) a.disconnect()
|
|
|
|
|
localStorage.removeItem('waste_last_network')
|
|
|
|
|
localStorage.removeItem('waste_nick')
|
|
|
|
|
localStorage.removeItem('waste_anchor_url')
|
|
|
|
|
if (clearIdentity) localStorage.removeItem('waste_seed')
|
|
|
|
|
window.location.reload()
|
|
|
|
|
},
|
|
|
|
|
|
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>
2026-06-25 20:49:15 +02:00
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
Add peer-to-peer file transfer in browser mode
Implements both pull (browse & download from a shared folder) and push
(send a file directly to a peer) using the yaw2 file protocol over WebRTC
DataChannels.
- PeerConn: handle browse/files/get/file-offer/file-accept/file-done
control messages; stream files in 64KB chunks over f:xid binary
DataChannels; auto-accept incoming offers (push and pull)
- PeerConn.sendFilePush(): initiate an outbound file transfer without
requiring the peer to browse first
- BrowserAdapter: sharedFiles map, setSharedFiles(), requestBrowse(),
requestGet(), sendFileTo() — propagates share map to all active peers
- Store: activeFilePeer, setActiveFilePeer, setSharedFiles, browseFiles,
sendFileTo; file_complete handler triggers browser download automatically
- FileBrowser component: third-column panel listing a peer's shared files
with name, size, and a download button
- FolderPicker component: webkitdirectory input in the sidebar (browser
mode only) for selecting a local folder to share
- Sidebar: 📎 send-file button on peer rows (hover) with inline file picker
- App.tsx: use browser mode when WASTE_CONFIG.signalURL is set, even on
localhost — allows testing browser mode via npm run dev with config.js
- deploy-daemon.sh: kill existing daemon before scp to avoid upload failure
when the binary is locked; remove duplicate kill block in restart step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 20:29:54 +02:00
|
|
|
browseFiles(peerId) {
|
|
|
|
|
const a = get().adapter
|
|
|
|
|
if (a instanceof BrowserAdapter) {
|
|
|
|
|
a.requestBrowse(peerId)
|
|
|
|
|
} else {
|
|
|
|
|
get().send({ type: 'get_file_list', peer_id: peerId as unknown as import('../types').PeerID })
|
|
|
|
|
}
|
|
|
|
|
set({ activeFilePeer: peerId })
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-22 21:57:32 +02:00
|
|
|
handleEvent(msg) {
|
|
|
|
|
switch (msg.type) {
|
|
|
|
|
case 'state_snapshot': {
|
|
|
|
|
const networks = msg.networks ?? []
|
|
|
|
|
set({
|
2026-06-22 23:22:32 +02:00
|
|
|
masterAlias: msg.master_alias ?? null,
|
|
|
|
|
masterId: msg.master_id ?? null,
|
2026-06-22 21:57:32 +02:00
|
|
|
localPeer: msg.local_peer ?? null,
|
|
|
|
|
networks,
|
|
|
|
|
connectedPeers: msg.connected_peers ?? [],
|
|
|
|
|
activeNetworkId: networks[0]?.network_id ?? null,
|
|
|
|
|
})
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
case 'network_joined': {
|
|
|
|
|
set(s => ({
|
2026-06-23 00:06:41 +02:00
|
|
|
localPeer: s.localPeer ?? msg.local_peer ?? null,
|
2026-06-22 21:57:32 +02:00
|
|
|
networks: s.networks.some(n => n.network_id === msg.network_id)
|
|
|
|
|
? s.networks
|
|
|
|
|
: [...s.networks, {
|
|
|
|
|
network_id: msg.network_id!,
|
|
|
|
|
network_name: msg.network_name!,
|
2026-06-23 00:06:41 +02:00
|
|
|
local_peer: msg.local_peer,
|
2026-06-22 21:57:32 +02:00
|
|
|
share_dir: msg.share_dir,
|
|
|
|
|
}],
|
|
|
|
|
activeNetworkId: s.activeNetworkId ?? msg.network_id!,
|
|
|
|
|
}))
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
case 'network_left': {
|
|
|
|
|
set(s => ({
|
|
|
|
|
networks: s.networks.filter(n => n.network_id !== msg.network_id),
|
|
|
|
|
activeNetworkId: s.activeNetworkId === msg.network_id
|
|
|
|
|
? (s.networks.find(n => n.network_id !== msg.network_id)?.network_id ?? null)
|
|
|
|
|
: s.activeNetworkId,
|
|
|
|
|
}))
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
case 'peer_connected': {
|
|
|
|
|
if (msg.peer) {
|
|
|
|
|
set(s => ({
|
|
|
|
|
connectedPeers: s.connectedPeers.some(p => p.id === msg.peer!.id)
|
2026-06-25 20:54:47 +02:00
|
|
|
? s.connectedPeers.map(p => p.id === msg.peer!.id ? { ...p, ...msg.peer } : p)
|
2026-06-22 21:57:32 +02:00
|
|
|
: [...s.connectedPeers, msg.peer!],
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
2026-06-22 23:22:32 +02:00
|
|
|
case 'session_ready': {
|
|
|
|
|
if (msg.peer_id && msg.nick) {
|
|
|
|
|
set(s => ({
|
|
|
|
|
connectedPeers: s.connectedPeers.map(p =>
|
|
|
|
|
p.id === msg.peer_id ? { ...p, alias: msg.nick! } : p
|
|
|
|
|
),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
2026-06-22 21:57:32 +02:00
|
|
|
case 'peer_disconnected': {
|
|
|
|
|
set(s => ({
|
|
|
|
|
connectedPeers: s.connectedPeers.filter(p => p.id !== msg.peer_id),
|
|
|
|
|
}))
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
case 'message_received': {
|
|
|
|
|
if (msg.message) {
|
2026-06-22 23:22:32 +02:00
|
|
|
const m = msg.message
|
|
|
|
|
const room = m.room
|
2026-06-25 13:28:40 +02:00
|
|
|
const fromId = String(m.from)
|
2026-06-22 23:22:32 +02:00
|
|
|
set(s => {
|
|
|
|
|
const existing = s.messages[room] ?? []
|
|
|
|
|
if (m.mid && existing.some(e => e.mid === m.mid)) return s
|
2026-06-25 13:28:40 +02:00
|
|
|
const prev = s.peerStatus[fromId] ?? {}
|
|
|
|
|
return {
|
|
|
|
|
messages: { ...s.messages, [room]: [...existing, m] },
|
|
|
|
|
peerStatus: { ...s.peerStatus, [fromId]: { ...prev, lastSeen: m.ts } },
|
|
|
|
|
}
|
2026-06-22 23:22:32 +02:00
|
|
|
})
|
2026-06-22 21:57:32 +02:00
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
2026-06-25 13:28:40 +02:00
|
|
|
case 'peer_status': {
|
|
|
|
|
const pid = String(msg.peer_id)
|
|
|
|
|
set(s => {
|
|
|
|
|
const prev = s.peerStatus[pid] ?? {}
|
|
|
|
|
return {
|
|
|
|
|
peerStatus: {
|
|
|
|
|
...s.peerStatus,
|
|
|
|
|
[pid]: {
|
|
|
|
|
...prev,
|
|
|
|
|
...(msg.conn_state ? { connState: msg.conn_state } : {}),
|
|
|
|
|
...(msg.candidate_type ? { candidateType: msg.candidate_type } : {}),
|
|
|
|
|
...(msg.remote_address !== undefined ? { remoteAddress: msg.remote_address } : {}),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
break
|
|
|
|
|
}
|
2026-06-22 23:22:32 +02:00
|
|
|
case 'identity_exported': {
|
|
|
|
|
if (msg.backup) set({ exportedBackup: msg.backup })
|
|
|
|
|
break
|
|
|
|
|
}
|
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>
2026-06-25 20:49:15 +02:00
|
|
|
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
|
|
|
|
|
}
|
2026-06-22 21:57:32 +02:00
|
|
|
case 'file_list': {
|
|
|
|
|
if (msg.peer_id && msg.files) {
|
|
|
|
|
set(s => ({
|
|
|
|
|
fileLists: { ...s.fileLists, [msg.peer_id!]: msg.files! },
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
Add peer-to-peer file transfer in browser mode
Implements both pull (browse & download from a shared folder) and push
(send a file directly to a peer) using the yaw2 file protocol over WebRTC
DataChannels.
- PeerConn: handle browse/files/get/file-offer/file-accept/file-done
control messages; stream files in 64KB chunks over f:xid binary
DataChannels; auto-accept incoming offers (push and pull)
- PeerConn.sendFilePush(): initiate an outbound file transfer without
requiring the peer to browse first
- BrowserAdapter: sharedFiles map, setSharedFiles(), requestBrowse(),
requestGet(), sendFileTo() — propagates share map to all active peers
- Store: activeFilePeer, setActiveFilePeer, setSharedFiles, browseFiles,
sendFileTo; file_complete handler triggers browser download automatically
- FileBrowser component: third-column panel listing a peer's shared files
with name, size, and a download button
- FolderPicker component: webkitdirectory input in the sidebar (browser
mode only) for selecting a local folder to share
- Sidebar: 📎 send-file button on peer rows (hover) with inline file picker
- App.tsx: use browser mode when WASTE_CONFIG.signalURL is set, even on
localhost — allows testing browser mode via npm run dev with config.js
- deploy-daemon.sh: kill existing daemon before scp to avoid upload failure
when the binary is locked; remove duplicate kill block in restart step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 20:29:54 +02:00
|
|
|
case 'file_complete': {
|
|
|
|
|
if (msg.path && msg.offer?.name) {
|
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>
2026-06-25 20:49:15 +02:00
|
|
|
// clear progress entry
|
|
|
|
|
const xid = msg.offer.xid
|
|
|
|
|
set(s => { const fp = { ...s.fileProgress }; delete fp[xid]; return { fileProgress: fp } })
|
Add peer-to-peer file transfer in browser mode
Implements both pull (browse & download from a shared folder) and push
(send a file directly to a peer) using the yaw2 file protocol over WebRTC
DataChannels.
- PeerConn: handle browse/files/get/file-offer/file-accept/file-done
control messages; stream files in 64KB chunks over f:xid binary
DataChannels; auto-accept incoming offers (push and pull)
- PeerConn.sendFilePush(): initiate an outbound file transfer without
requiring the peer to browse first
- BrowserAdapter: sharedFiles map, setSharedFiles(), requestBrowse(),
requestGet(), sendFileTo() — propagates share map to all active peers
- Store: activeFilePeer, setActiveFilePeer, setSharedFiles, browseFiles,
sendFileTo; file_complete handler triggers browser download automatically
- FileBrowser component: third-column panel listing a peer's shared files
with name, size, and a download button
- FolderPicker component: webkitdirectory input in the sidebar (browser
mode only) for selecting a local folder to share
- Sidebar: 📎 send-file button on peer rows (hover) with inline file picker
- App.tsx: use browser mode when WASTE_CONFIG.signalURL is set, even on
localhost — allows testing browser mode via npm run dev with config.js
- deploy-daemon.sh: kill existing daemon before scp to avoid upload failure
when the binary is locked; remove duplicate kill block in restart step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 20:29:54 +02:00
|
|
|
const a = document.createElement('a')
|
|
|
|
|
a.href = msg.path
|
|
|
|
|
a.download = msg.offer.name
|
|
|
|
|
a.click()
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
2026-06-22 21:57:32 +02:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}))
|