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>
This commit is contained in:
Fredrik Johansson
2026-06-25 20:29:54 +02:00
parent ea66e2eb58
commit 91b7406d01
8 changed files with 317 additions and 18 deletions

View File

@@ -1,5 +1,6 @@
import { useWaste } from '../store'
import type { PeerStatus } from '../store'
import { FolderPicker } from './FolderPicker'
function makeYawCard(id: string, alias: string): string {
const nick = encodeURIComponent(alias.trim().slice(0, 40))
@@ -28,7 +29,7 @@ export function Sidebar() {
localPeer, masterId, masterAlias,
networks, activeNetworkId, activeRoom,
connectedPeers, peerStatus,
setActiveRoom, setActiveNetwork, messages, send,
setActiveRoom, setActiveNetwork, messages, browseFiles, sendFileTo, adapterMode,
} = useWaste()
const rooms = ['general']
@@ -45,7 +46,7 @@ export function Sidebar() {
}
function requestFiles(peerId: string) {
send({ type: 'get_file_list', network_id: activeNetworkId ?? undefined, peer_id: peerId })
browseFiles(peerId)
}
return (
@@ -86,6 +87,13 @@ export function Sidebar() {
))}
</div>
{adapterMode === 'browser' && (
<div className="sidebar-section">
<span className="sidebar-label">Sharing</span>
<div style={{ padding: '4px 12px' }}><FolderPicker /></div>
</div>
)}
<div className="sidebar-section sidebar-peers">
<span className="sidebar-label">Peers · {connectedPeers.length + 1}</span>
@@ -116,7 +124,15 @@ export function Sidebar() {
<span className="peer-row-id">{p.id.slice(0, 8)}</span>
<span className="peer-row-actions">
<button className="peer-action" onClick={() => openDM(p.id)} title="DM"></button>
<button className="peer-action" onClick={() => requestFiles(p.id)} title="Files"></button>
<button className="peer-action" onClick={() => requestFiles(p.id)} title="Browse files"></button>
<label className="peer-action" title="Send file" style={{ cursor: 'pointer' }}>
📎
<input type="file" style={{ display: 'none' }} onChange={e => {
const f = e.target.files?.[0]
if (f) sendFileTo(p.id, f)
e.target.value = ''
}} />
</label>
</span>
</div>
)