feat: multi-network join, persistence, and switching in web UI

- Sidebar: "+" button next to Networks opens an inline join form;
  in browser mode shows anchor URL field; saves to waste_saved_networks
- Onboarding: saved networks shown as quick-join chips above the join
  form; auto-rejoin all saved networks on reconnect (not just one)
- Browser mode: networks persisted as [{name, anchor}] array in
  localStorage (waste_saved_networks); legacy waste_last_network kept
  for backward compat
- Daemon mode: daemon already tracks joined networks; web UI just needs
  the "+" to send join_network for additional ones

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-06-29 16:38:46 +02:00
parent 1d9827beb0
commit dab5387cbd
3 changed files with 87 additions and 9 deletions

View File

@@ -32,10 +32,13 @@ export function Sidebar() {
networks, activeNetworkId, activeRoom,
connectedPeers, peerStatus,
setActiveRoom, setActiveNetwork, messages, browseFiles, sendFileTo, adapterMode,
customRooms, createRoom, logout,
customRooms, createRoom, logout, send,
} = useWaste()
const [addingRoom, setAddingRoom] = useState(false)
const [newRoomName, setNewRoomName] = useState('')
const [addingNetwork, setAddingNetwork] = useState(false)
const [newNetName, setNewNetName] = useState('')
const [newNetAnchor, setNewNetAnchor] = useState('')
const netCustomRooms = activeNetworkId ? (customRooms[activeNetworkId] ?? []) : []
const rooms = ['general', ...netCustomRooms]
@@ -50,6 +53,25 @@ export function Sidebar() {
setAddingRoom(false)
}
function submitNewNetwork(e: React.FormEvent) {
e.preventDefault()
const name = newNetName.trim()
if (!name) return
if (adapterMode === 'browser') {
// Persist to saved networks list in localStorage.
const anchor = newNetAnchor.trim() || localStorage.getItem('waste_anchor_url') || ''
const saved: Array<{name: string; anchor: string}> = JSON.parse(localStorage.getItem('waste_saved_networks') ?? '[]')
if (!saved.some(n => n.name === name && n.anchor === anchor)) {
saved.push({ name, anchor })
localStorage.setItem('waste_saved_networks', JSON.stringify(saved))
}
}
send({ type: 'join_network', network_name: name })
setNewNetName('')
setNewNetAnchor('')
setAddingNetwork(false)
}
const displayAlias = localPeer?.alias ?? masterAlias ?? ''
const displayId = localPeer?.id ?? masterId ?? ''
const card = displayId ? makeYawCard(displayId, displayAlias) : null
@@ -95,9 +117,12 @@ export function Sidebar() {
<div className="sidebar-section">
<div className="sidebar-label-row">
<span className="sidebar-label">Networks</span>
{activeNetworkId && (
<button className="sidebar-add" onClick={copyHangLink} title="Copy hang link (pre-fills join form, no invite required)">🔗</button>
)}
<span style={{ display: 'flex', gap: 2 }}>
{activeNetworkId && (
<button className="sidebar-add" onClick={copyHangLink} title="Copy hang link">🔗</button>
)}
<button className="sidebar-add" onClick={() => setAddingNetwork(v => !v)} title="Join network">+</button>
</span>
</div>
{networks.map(n => (
<button
@@ -108,6 +133,26 @@ export function Sidebar() {
{n.network_name}
</button>
))}
{addingNetwork && (
<form className="sidebar-new-room" onSubmit={submitNewNetwork}>
<input
autoFocus
value={newNetName}
onChange={e => setNewNetName(e.target.value)}
placeholder="network name"
onKeyDown={e => e.key === 'Escape' && (setAddingNetwork(false), setNewNetName(''))}
/>
{adapterMode === 'browser' && (
<input
value={newNetAnchor}
onChange={e => setNewNetAnchor(e.target.value)}
placeholder="anchor URL (blank = current)"
className="mono"
style={{ fontSize: '0.75rem', marginTop: 4 }}
/>
)}
</form>
)}
</div>
<div className="sidebar-section">