Presence chirps now carry the friendly device name, and the engine tracks the present set as Peers (id/name/source/room). New Engine.present() -> [Peer] exposes who's online in the room across any network. Both apps poll it and show a green-dot row of online devices above the feed. The present set already powered prefer-direct's coverage check; this enriches it with names and surfaces it. Verified live on macOS: an agent in the room shows as "● Patrick's NAS". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
210 lines
7.3 KiB
Swift
210 lines
7.3 KiB
Swift
import SwiftUI
|
|
|
|
struct ContentView: View {
|
|
@Bindable var store: MacTetherStore
|
|
@State private var draft = ""
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
toolbar
|
|
Divider()
|
|
if store.connected && !store.present.isEmpty {
|
|
presenceRow
|
|
Divider()
|
|
}
|
|
if store.connected && !store.nearby.isEmpty {
|
|
nearbyStrip
|
|
Divider()
|
|
}
|
|
feedArea
|
|
Divider()
|
|
if !store.receipts.isEmpty {
|
|
seenByBar
|
|
Divider()
|
|
}
|
|
composer
|
|
}
|
|
.frame(minWidth: 480, minHeight: 400)
|
|
.task { store.connectIfSaved() }
|
|
}
|
|
|
|
// MARK: - Delivery receipts
|
|
|
|
private var seenByBar: some View {
|
|
let names = Array(Set(store.receipts.map(\.name))).sorted()
|
|
return HStack(spacing: 6) {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.foregroundStyle(.green).font(.caption)
|
|
Text("Seen by \(names.joined(separator: ", "))")
|
|
.font(.caption).foregroundStyle(.secondary).lineLimit(1)
|
|
Spacer()
|
|
}
|
|
.padding(.horizontal, 14).padding(.vertical, 5)
|
|
}
|
|
|
|
// MARK: - Presence ("who's here")
|
|
|
|
private var presenceRow: some View {
|
|
HStack(spacing: 12) {
|
|
ForEach(store.present, id: \.id) { peer in
|
|
HStack(spacing: 5) {
|
|
Circle().fill(.green).frame(width: 7, height: 7)
|
|
Image(systemName: sourceIcon(peer.source)).font(.caption2)
|
|
Text(peer.name).font(.caption).foregroundStyle(.secondary).lineLimit(1)
|
|
}
|
|
}
|
|
Spacer()
|
|
}
|
|
.padding(.horizontal, 14).padding(.vertical, 5)
|
|
}
|
|
|
|
// 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 {
|
|
HStack(spacing: 10) {
|
|
Circle()
|
|
.fill(store.connected ? Color.green : Color.secondary.opacity(0.5))
|
|
.frame(width: 10, height: 10)
|
|
.animation(.easeInOut, value: store.connected)
|
|
|
|
if store.connected {
|
|
Text("Room \(store.room)").font(.callout).foregroundStyle(.secondary)
|
|
} else {
|
|
TextField("Server URL", text: $store.serverURL)
|
|
.textFieldStyle(.roundedBorder)
|
|
.frame(maxWidth: 240)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
if let err = store.lastError, store.hasAttemptedConnect {
|
|
Text(err).font(.caption).foregroundStyle(.red)
|
|
}
|
|
|
|
Button(store.connected ? "Disconnect" : "Connect") {
|
|
store.connected ? store.disconnect() : store.connect()
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.tint(store.connected ? .secondary : .accentColor)
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 10)
|
|
}
|
|
|
|
// MARK: - Feed
|
|
|
|
private var feedArea: some View {
|
|
List(store.feed) { msg in
|
|
feedRow(msg)
|
|
.listRowSeparator(.hidden)
|
|
.listRowInsets(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12))
|
|
}
|
|
.listStyle(.plain)
|
|
.overlay {
|
|
if store.feed.isEmpty {
|
|
ContentUnavailableView(
|
|
"No messages yet",
|
|
systemImage: "link.circle",
|
|
description: Text(store.connected
|
|
? "Copy anything on a connected device — it'll appear here."
|
|
: "Connect to your tether server to start syncing.")
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func feedRow(_ item: FeedItem) -> some View {
|
|
HStack(alignment: .top, spacing: 10) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(item.text)
|
|
.font(.body)
|
|
.lineLimit(6)
|
|
.textSelection(.enabled)
|
|
HStack {
|
|
Label(item.source, systemImage: sourceIcon(item.source))
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
Spacer()
|
|
Text(item.date, style: .time)
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
Button {
|
|
store.copyToPasteboard(item)
|
|
} label: {
|
|
Image(systemName: "doc.on.doc")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.help("Copy to clipboard")
|
|
}
|
|
.padding(10)
|
|
.background(Color.primary.opacity(0.04), in: RoundedRectangle(cornerRadius: 8))
|
|
}
|
|
|
|
private func sourceIcon(_ source: String) -> String {
|
|
let s = source
|
|
if s.contains("iphone") || s.contains("ios") { return "iphone" }
|
|
if s.contains("ipad") { return "ipad" }
|
|
if s.contains("macos") || s.contains("mac") { return "laptopcomputer" }
|
|
if s.contains("windows") { return "pc" }
|
|
return "desktopcomputer"
|
|
}
|
|
|
|
// MARK: - Composer
|
|
|
|
private var composer: some View {
|
|
HStack(spacing: 10) {
|
|
TextField("Type a message…", text: $draft, axis: .vertical)
|
|
.textFieldStyle(.roundedBorder)
|
|
.lineLimit(1...4)
|
|
.onSubmit {
|
|
guard !draft.isEmpty else { return }
|
|
store.send(draft); draft = ""
|
|
}
|
|
Button {
|
|
store.send(draft); draft = ""
|
|
} label: {
|
|
Image(systemName: "paperplane.fill")
|
|
}
|
|
.keyboardShortcut(.return, modifiers: .command)
|
|
.buttonStyle(.borderedProminent)
|
|
.disabled(!store.connected || draft.isEmpty)
|
|
}
|
|
.padding(12)
|
|
}
|
|
}
|