ios: Nearby-devices picker (AirDrop-style), matching the Mac
Port the nearby picker to iOS: the store polls engine.nearby() while connected and RoomView shows discovered devices as tap-to-join chips with platform icons, same as MacTetherApp. pair() joins the device's room. Dormant on iOS until the multicast entitlement lands (Rust mdns-sd uses a raw multicast socket iOS blocks without it) — the UI is ready; it'll populate once the entitlement is granted. No effect when the list is empty. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,10 @@ struct RoomView: View {
|
||||
VStack(spacing: 0) {
|
||||
connectionBar
|
||||
Divider()
|
||||
if store.connected && !store.nearby.isEmpty {
|
||||
nearbyStrip
|
||||
Divider()
|
||||
}
|
||||
feedList
|
||||
Divider()
|
||||
composer
|
||||
@@ -50,6 +54,46 @@ struct RoomView: View {
|
||||
.padding()
|
||||
}
|
||||
|
||||
// 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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12).padding(.vertical, 6)
|
||||
}
|
||||
}
|
||||
|
||||
private func sourceIcon(_ source: String) -> String {
|
||||
if source.contains("iphone") || source.contains("ios") { return "iphone" }
|
||||
if source.contains("ipad") { return "ipad" }
|
||||
if source.contains("mac") { return "laptopcomputer" }
|
||||
if source.contains("windows") { return "pc" }
|
||||
return "desktopcomputer"
|
||||
}
|
||||
|
||||
private var feedList: some View {
|
||||
List(store.feed) { msg in
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
|
||||
@@ -30,12 +30,14 @@ final class TetherStore {
|
||||
return a.isEmpty ? RoomIdentity.derive() : roomForAccount(account: a)
|
||||
}()
|
||||
var feed: [FeedItem] = []
|
||||
var nearby: [Peer] = [] // tether devices on the LAN (empty on iOS w/o multicast entitlement)
|
||||
var connected = false
|
||||
var lastError: String?
|
||||
|
||||
private let deviceID = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
|
||||
private var engine: Engine?
|
||||
private var bridge: EngineBridge?
|
||||
private var nearbyTask: Task<Void, Never>?
|
||||
|
||||
func connectIfSaved() {
|
||||
guard UserDefaults.standard.string(forKey: "serverURL") != nil else { return }
|
||||
@@ -58,15 +60,33 @@ final class TetherStore {
|
||||
engine = e
|
||||
bridge = b
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
engine?.stop()
|
||||
engine = nil
|
||||
bridge = 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()
|
||||
}
|
||||
|
||||
/// Read the system pasteboard (foreground only — iOS bans background reads)
|
||||
/// and push it to the room.
|
||||
func sendClipboard() {
|
||||
|
||||
Reference in New Issue
Block a user