macos: Nearby-devices picker (AirDrop-style) over the LAN

Add a horizontal strip of discovered tether devices to the window: the store
polls engine.nearby() every 2s while connected and renders each as a chip with
its friendly name + platform icon. Tapping a device calls pair() — sets room to
that device's room and reconnects, so you join it without typing a room id.
Devices already in your room show "in your room" (disabled).

Verified live on macOS: two named LAN devices appear as "tap to join" chips
once the app has Local Network permission.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 01:26:37 -05:00
parent 6e24d19a58
commit 049456521f
2 changed files with 58 additions and 0 deletions

View File

@@ -8,6 +8,10 @@ struct ContentView: View {
VStack(spacing: 0) {
toolbar
Divider()
if store.connected && !store.nearby.isEmpty {
nearbyStrip
Divider()
}
feedArea
Divider()
composer
@@ -16,6 +20,40 @@ struct ContentView: View {
.task { store.connectIfSaved() }
}
// MARK: - Nearby devices (AirDrop-style picker)
private var nearbyStrip: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
Label("Nearby", systemImage: "dot.radiowaves.left.and.right")
.font(.caption).foregroundStyle(.secondary)
.padding(.trailing, 4)
ForEach(store.nearby, id: \.id) { peer in
let here = peer.room == store.room
Button {
store.pair(with: peer)
} label: {
HStack(spacing: 6) {
Image(systemName: sourceIcon(peer.source))
VStack(alignment: .leading, spacing: 1) {
Text(peer.name).font(.caption).lineLimit(1)
Text(here ? "in your room" : "tap to join")
.font(.caption2)
.foregroundStyle(here ? .green : .secondary)
}
}
.padding(.horizontal, 10).padding(.vertical, 6)
.background(Color.primary.opacity(0.06), in: Capsule())
}
.buttonStyle(.plain)
.disabled(here)
.help(here ? "Already in this room" : "Join \(peer.name)'s room")
}
}
.padding(.horizontal, 12).padding(.vertical, 6)
}
}
// MARK: - Toolbar
private var toolbar: some View {

View File

@@ -19,6 +19,7 @@ final class MacTetherStore {
var serverURL: String = UserDefaults.standard.string(forKey: "serverURL") ?? "https://tether.pecord.io"
var room: String = RoomIdentity.derive()
var feed: [FeedItem] = []
var nearby: [Peer] = [] // tether devices on the LAN, for the picker
var connected = false
var lastError: String?
var hasAttemptedConnect = false
@@ -27,6 +28,7 @@ final class MacTetherStore {
private var engine: Engine?
private var bridge: EngineBridge?
private var pollTask: Task<Void, Never>?
private var nearbyTask: Task<Void, Never>?
private var lastChangeCount = NSPasteboard.general.changeCount
func connectIfSaved() {
@@ -53,6 +55,15 @@ final class MacTetherStore {
connected = true
lastError = nil
// Poll the engine's mDNS-discovered device list for the picker.
nearbyTask = Task { [weak self] in
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(2))
guard let self, let engine = self.engine else { continue }
self.nearby = engine.nearby().sorted { $0.name < $1.name }
}
}
// Send: poll NSPasteboard for local changes push to room.
lastChangeCount = NSPasteboard.general.changeCount
pollTask = Task { [weak self] in
@@ -75,9 +86,18 @@ final class MacTetherStore {
bridge = nil
pollTask?.cancel()
pollTask = nil
nearbyTask?.cancel()
nearbyTask = nil
nearby = []
connected = false
}
/// Join a discovered device's room the "pair" action from the picker.
func pair(with peer: Peer) {
room = peer.room
connect()
}
func send(_ text: String) {
engine?.send(text: text)
}