apps: show delivery receipts — "✓ Seen by <device>"
Both apps now poll engine.receipts() alongside nearby() and render a "Seen by <names>" bar above the composer when acks come in. Closes the loop the receipt engine opened: you can see your clipboard was received, instead of asking. Verified live on macOS: copying on the Mac with a peer in the room shows "✓ Seen by Rust LAN Peer". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,12 +14,30 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
feedArea
|
feedArea
|
||||||
Divider()
|
Divider()
|
||||||
|
if !store.receipts.isEmpty {
|
||||||
|
seenByBar
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
composer
|
composer
|
||||||
}
|
}
|
||||||
.frame(minWidth: 480, minHeight: 400)
|
.frame(minWidth: 480, minHeight: 400)
|
||||||
.task { store.connectIfSaved() }
|
.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: - Nearby devices (AirDrop-style picker)
|
// MARK: - Nearby devices (AirDrop-style picker)
|
||||||
|
|
||||||
private var nearbyStrip: some View {
|
private var nearbyStrip: some View {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ final class MacTetherStore {
|
|||||||
}()
|
}()
|
||||||
var feed: [FeedItem] = []
|
var feed: [FeedItem] = []
|
||||||
var nearby: [Peer] = [] // tether devices on the LAN, for the picker
|
var nearby: [Peer] = [] // tether devices on the LAN, for the picker
|
||||||
|
var receipts: [Receipt] = [] // delivery acks — "seen by <device>"
|
||||||
var connected = false
|
var connected = false
|
||||||
var lastError: String?
|
var lastError: String?
|
||||||
var hasAttemptedConnect = false
|
var hasAttemptedConnect = false
|
||||||
@@ -68,6 +69,7 @@ final class MacTetherStore {
|
|||||||
try? await Task.sleep(for: .seconds(2))
|
try? await Task.sleep(for: .seconds(2))
|
||||||
guard let self, let engine = self.engine else { continue }
|
guard let self, let engine = self.engine else { continue }
|
||||||
self.nearby = engine.nearby().sorted { $0.name < $1.name }
|
self.nearby = engine.nearby().sorted { $0.name < $1.name }
|
||||||
|
self.receipts = engine.receipts()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ struct RoomView: View {
|
|||||||
}
|
}
|
||||||
feedList
|
feedList
|
||||||
Divider()
|
Divider()
|
||||||
|
if !store.receipts.isEmpty {
|
||||||
|
seenByBar
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
composer
|
composer
|
||||||
}
|
}
|
||||||
.navigationTitle("tether")
|
.navigationTitle("tether")
|
||||||
@@ -86,6 +90,18 @@ struct RoomView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
private func sourceIcon(_ source: String) -> String {
|
private func sourceIcon(_ source: String) -> String {
|
||||||
if source.contains("iphone") || source.contains("ios") { return "iphone" }
|
if source.contains("iphone") || source.contains("ios") { return "iphone" }
|
||||||
if source.contains("ipad") { return "ipad" }
|
if source.contains("ipad") { return "ipad" }
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ final class TetherStore {
|
|||||||
}()
|
}()
|
||||||
var feed: [FeedItem] = []
|
var feed: [FeedItem] = []
|
||||||
var nearby: [Peer] = [] // tether devices on the LAN (empty on iOS w/o multicast entitlement)
|
var nearby: [Peer] = [] // tether devices on the LAN (empty on iOS w/o multicast entitlement)
|
||||||
|
var receipts: [Receipt] = [] // delivery acks — "seen by <device>"
|
||||||
var connected = false
|
var connected = false
|
||||||
var lastError: String?
|
var lastError: String?
|
||||||
|
|
||||||
@@ -67,6 +68,7 @@ final class TetherStore {
|
|||||||
try? await Task.sleep(for: .seconds(2))
|
try? await Task.sleep(for: .seconds(2))
|
||||||
guard let self, let engine = self.engine else { continue }
|
guard let self, let engine = self.engine else { continue }
|
||||||
self.nearby = engine.nearby().sorted { $0.name < $1.name }
|
self.nearby = engine.nearby().sorted { $0.name < $1.name }
|
||||||
|
self.receipts = engine.receipts()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user