Files
tether/ios/MacTetherApp/Sources/MenuView.swift
Patrick Ecord db64b3b9a6 macos: migrate app to the shared tethercore engine (SSE + RTC)
Drop TetherKit's TetherClient/SSEClient/Message from the macOS app; drive
tethercore.Engine instead (RoomIdentity still from TetherKit via selective
import). The Mac now gets direct P2P over the RTC data channel for free, and
the native Swift app is no longer a parallel wire-protocol implementation.

Background NSPasteboard polling (auto-send on copy) and write-on-receive are
preserved. Feed UI moves to a FeedItem view-model (RTC messages carry ts=0 →
fall back to local time).

Verified end-to-end against the live server: RECEIVE (bus → Mac pasteboard via
pbpaste) and SEND (pbcopy → bus) both confirmed.

Generalize core/build-ios.sh → build-apple.sh: builds iOS device + sim + macOS
arm64 into one TetherCore.xcframework and vendors it into both apps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 23:31:54 -05:00

101 lines
3.6 KiB
Swift

import SwiftUI
struct MenuView: View {
@Bindable var store: MacTetherStore
@State private var draft = ""
var body: some View {
VStack(spacing: 0) {
header
Divider()
feedList
Divider()
composer
}
.padding(8)
.task { store.connectIfSaved() }
}
private var header: some View {
VStack(alignment: .leading, spacing: 6) {
HStack {
Circle()
.fill(store.connected ? Color.green : Color.secondary)
.frame(width: 8, height: 8)
Text(store.connected ? "connected · \(store.room)" : "disconnected")
.font(.caption).foregroundStyle(.secondary)
Spacer()
Button(store.connected ? "Disconnect" : "Connect") {
store.connected ? store.disconnect() : store.connect()
}.font(.caption)
}
if !store.connected {
TextField("http://tether.lan:8765", text: $store.serverURL)
.textFieldStyle(.roundedBorder).font(.caption)
Button("Connect") { store.connect() }
.buttonStyle(.borderedProminent).controlSize(.small)
.frame(maxWidth: .infinity)
}
if store.hasAttemptedConnect, let err = store.lastError {
Text(err).font(.caption2).foregroundStyle(.red)
}
}
.padding(.bottom, 6)
}
private var feedList: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 4) {
if store.feed.isEmpty {
Text("No messages yet — clipboard changes auto-send when connected.")
.font(.caption).foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top, 20)
} else {
ForEach(store.feed) { msg in
feedRow(msg)
}
}
}
.padding(.vertical, 6)
}
.frame(minHeight: 120, maxHeight: 240)
}
private func feedRow(_ item: FeedItem) -> some View {
HStack(alignment: .top, spacing: 6) {
VStack(alignment: .leading, spacing: 2) {
Text(item.text)
.font(.caption).lineLimit(3)
HStack {
Text(item.source).font(.caption2).foregroundStyle(.secondary)
Spacer()
Text(item.date, style: .time).font(.caption2).foregroundStyle(.secondary)
}
}
Button { store.copyToPasteboard(item) } label: {
Image(systemName: "doc.on.doc").font(.caption2)
}
.buttonStyle(.plain).foregroundStyle(.secondary)
}
.padding(6)
.background(Color.primary.opacity(0.04), in: RoundedRectangle(cornerRadius: 6))
}
private var composer: some View {
HStack(spacing: 6) {
TextField("Send text…", text: $draft)
.textFieldStyle(.roundedBorder).font(.caption)
.onSubmit { if !draft.isEmpty { store.send(draft); draft = "" } }
Button {
store.send(draft); draft = ""
} label: {
Image(systemName: "paperplane.fill")
}
.disabled(!store.connected || draft.isEmpty)
.buttonStyle(.borderedProminent).controlSize(.small)
}
.padding(.top, 6)
}
}