nvrsion: Add: Block Sleep feature, forward approval to phone in settings, refactor sync message handling, and update related files.
Nucleic-Promote: 1 Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
@@ -95,6 +95,11 @@ final class HostConnection {
|
|||||||
/// A join code this host minted at our request ("add a device to this mesh") — the
|
/// A join code this host minted at our request ("add a device to this mesh") — the
|
||||||
/// `nucleic://pair?d=…` string to show as a QR / copyable code, or nil if it couldn't.
|
/// `nucleic://pair?d=…` string to show as a QR / copyable code, or nil if it couldn't.
|
||||||
var pairingCodeReceived: (String?) -> Void = { _ in }
|
var pairingCodeReceived: (String?) -> Void = { _ in }
|
||||||
|
/// This host forwarded a Mac-pair confirm — a Mac is joining via a code we shared and
|
||||||
|
/// wants the user's allow/deny. The phone can approve it (`respondMacPair`).
|
||||||
|
var macPairRequested: (WireMacPairRequest) -> Void = { _ in }
|
||||||
|
/// The forwarded Mac-pair (by deviceID) was answered/withdrawn — dismiss the prompt.
|
||||||
|
var macPairResolved: (String) -> Void = { _ in }
|
||||||
}
|
}
|
||||||
private let callbacks: Callbacks
|
private let callbacks: Callbacks
|
||||||
|
|
||||||
@@ -600,6 +605,13 @@ final class HostConnection {
|
|||||||
// This Mac minted a join code we asked for ("add a device to this mesh") — hand it up
|
// This Mac minted a join code we asked for ("add a device to this mesh") — hand it up
|
||||||
// to RemoteStore for the QR/copy sheet.
|
// to RemoteStore for the QR/copy sheet.
|
||||||
callbacks.pairingCodeReceived(qr)
|
callbacks.pairingCodeReceived(qr)
|
||||||
|
case .macPairRequested(let req):
|
||||||
|
// A Mac is joining via a code we shared and needs allow/deny — forward it up so the
|
||||||
|
// phone can approve the join.
|
||||||
|
callbacks.macPairRequested(req)
|
||||||
|
case .macPairResolved(let deviceID, _):
|
||||||
|
// Answered on this Mac or another device (or timed out) — dismiss the prompt.
|
||||||
|
callbacks.macPairResolved(deviceID)
|
||||||
case .wireError(let error):
|
case .wireError(let error):
|
||||||
if error.code == .channelMismatch {
|
if error.code == .channelMismatch {
|
||||||
connectivity = .failed(error.message)
|
connectivity = .failed(error.message)
|
||||||
|
|||||||
@@ -92,6 +92,13 @@ final class RemoteStore: ObservableObject {
|
|||||||
/// its pairing window.
|
/// its pairing window.
|
||||||
private var pairingMintHostID: String?
|
private var pairingMintHostID: String?
|
||||||
|
|
||||||
|
/// A Mac trying to join via a code this phone shared, awaiting the user's allow/deny — the Mac
|
||||||
|
/// forwarded its pairing confirm here (`HostMsg.macPairRequested`) so it can be approved from
|
||||||
|
/// the phone. The "add a device" sheet renders an allow/deny dialog for it.
|
||||||
|
@Published private(set) var pendingMacPairRequest: WireMacPairRequest?
|
||||||
|
/// The host that forwarded the pending confirm, so the answer routes back to it.
|
||||||
|
private var pendingMacPairHostID: String?
|
||||||
|
|
||||||
/// A transient host-reported error (the mobile echo of the Mac's last-error bubble):
|
/// A transient host-reported error (the mobile echo of the Mac's last-error bubble):
|
||||||
/// shown as a red bubble at the bottom of the screen, auto-dismissed after a few seconds.
|
/// shown as a red bubble at the bottom of the screen, auto-dismissed after a few seconds.
|
||||||
struct LastError: Equatable, Identifiable {
|
struct LastError: Equatable, Identifiable {
|
||||||
@@ -561,6 +568,16 @@ final class RemoteStore: ObservableObject {
|
|||||||
guard case .requesting = self.addDevice else { return }
|
guard case .requesting = self.addDevice else { return }
|
||||||
self.addDevice = qr.map(AddDeviceState.ready) ?? .unavailable
|
self.addDevice = qr.map(AddDeviceState.ready) ?? .unavailable
|
||||||
}
|
}
|
||||||
|
cb.macPairRequested = { [weak self] req in
|
||||||
|
guard let self else { return }
|
||||||
|
self.pendingMacPairRequest = req
|
||||||
|
self.pendingMacPairHostID = hostID
|
||||||
|
}
|
||||||
|
cb.macPairResolved = { [weak self] deviceID in
|
||||||
|
guard let self, self.pendingMacPairRequest?.deviceID == deviceID else { return }
|
||||||
|
self.pendingMacPairRequest = nil
|
||||||
|
self.pendingMacPairHostID = nil
|
||||||
|
}
|
||||||
return cb
|
return cb
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -800,13 +817,25 @@ final class RemoteStore: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The user dismissed the "add a device" sheet — tell the minting Mac to close its pairing
|
/// The user dismissed the "add a device" sheet — tell the minting Mac to close its pairing
|
||||||
/// window (retire the one-time secret) and reset to idle.
|
/// window (retire the one-time secret) and reset to idle. Any Mac-join awaiting approval is
|
||||||
|
/// abandoned here too; the host fails it closed when its pairing window shuts.
|
||||||
func cancelPairingCode() {
|
func cancelPairingCode() {
|
||||||
if !demoMode, let id = pairingMintHostID { connections[id]?.send(.cancelPairingCode) }
|
if !demoMode, let id = pairingMintHostID { connections[id]?.send(.cancelPairingCode) }
|
||||||
pairingMintHostID = nil
|
pairingMintHostID = nil
|
||||||
|
pendingMacPairRequest = nil
|
||||||
|
pendingMacPairHostID = nil
|
||||||
addDevice = .idle
|
addDevice = .idle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Approve or deny a Mac joining via a code this phone shared (the forwarded confirm). Routes
|
||||||
|
/// the answer to the Mac that forwarded it and clears the prompt optimistically.
|
||||||
|
func respondMacPair(_ approve: Bool) {
|
||||||
|
guard let req = pendingMacPairRequest else { return }
|
||||||
|
if let id = pendingMacPairHostID { connections[id]?.send(.respondMacPair(req.deviceID, approve)) }
|
||||||
|
pendingMacPairRequest = nil
|
||||||
|
pendingMacPairHostID = nil
|
||||||
|
}
|
||||||
|
|
||||||
/// A stand-in join code for the offline demo so the QR/copy sheet renders without a Mac.
|
/// A stand-in join code for the offline demo so the QR/copy sheet renders without a Mac.
|
||||||
private static let demoPairingCode = "nucleic://pair?d=demo"
|
private static let demoPairingCode = "nucleic://pair?d=demo"
|
||||||
|
|
||||||
@@ -1009,7 +1038,7 @@ final class RemoteStore: ObservableObject {
|
|||||||
case .hello, .ping, .listPeers, .addressUpdate, .meshRoster,
|
case .hello, .ping, .listPeers, .addressUpdate, .meshRoster,
|
||||||
.registerLiveActivity, .endLiveActivity,
|
.registerLiveActivity, .endLiveActivity,
|
||||||
.transferOffer, .transferChunk, .transferCommit, .transferCancel, .fetchTranscript,
|
.transferOffer, .transferChunk, .transferCommit, .transferCancel, .fetchTranscript,
|
||||||
.requestPairingCode, .cancelPairingCode:
|
.requestPairingCode, .cancelPairingCode, .respondMacPair:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1120,7 +1149,7 @@ final class RemoteStore: ObservableObject {
|
|||||||
.transferOffer, .transferChunk, .transferCommit, .transferCancel,
|
.transferOffer, .transferChunk, .transferCommit, .transferCancel,
|
||||||
// "Add a device" mint is handled directly against a live host, not via demoHandle;
|
// "Add a device" mint is handled directly against a live host, not via demoHandle;
|
||||||
// the demo path short-circuits in `requestPairingCode()` with a stand-in code.
|
// the demo path short-circuits in `requestPairingCode()` with a stand-in code.
|
||||||
.requestPairingCode, .cancelPairingCode:
|
.requestPairingCode, .cancelPairingCode, .respondMacPair:
|
||||||
break // passive / already handled by the seeded fixtures (demo has no mesh peers)
|
break // passive / already handled by the seeded fixtures (demo has no mesh peers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -412,6 +412,22 @@ struct AddDeviceView: View {
|
|||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .confirmationAction) { Button("Done") { dismiss() } }
|
ToolbarItem(placement: .confirmationAction) { Button("Done") { dismiss() } }
|
||||||
}
|
}
|
||||||
|
// A Mac joining via this code needs the user's OK — the accepting Mac forwarded its
|
||||||
|
// confirm here so it can be approved from the phone (an iPhone/iPad joins hands-free).
|
||||||
|
.confirmationDialog(
|
||||||
|
"Let “\(store.pendingMacPairRequest?.label ?? "another Mac")” join?",
|
||||||
|
isPresented: Binding(
|
||||||
|
get: { store.pendingMacPairRequest != nil },
|
||||||
|
set: { if !$0 { store.respondMacPair(false) } }),
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
|
Button("Allow") { store.respondMacPair(true) }
|
||||||
|
Button("Don't allow", role: .destructive) { store.respondMacPair(false) }
|
||||||
|
Button("Cancel", role: .cancel) { store.respondMacPair(false) }
|
||||||
|
} message: {
|
||||||
|
Text("It joins the whole group — it gets the same control of your Macs' sessions an "
|
||||||
|
+ "iPhone does, and every Mac here stays connected.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Fire the request as the sheet appears; `onDismiss` (on the presenter) tells the Mac to
|
// Fire the request as the sheet appears; `onDismiss` (on the presenter) tells the Mac to
|
||||||
// close its pairing window.
|
// close its pairing window.
|
||||||
@@ -458,7 +474,8 @@ struct AddDeviceView: View {
|
|||||||
Text("Or send a code")
|
Text("Or send a code")
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("Paste into “Join mesh…” on another Mac, or “Enter code manually” on an iPhone or "
|
Text("Paste into “Join mesh…” on another Mac, or “Enter code manually” on an iPhone or "
|
||||||
+ "iPad. A Mac joining is confirmed on the Mac that shared the code.")
|
+ "iPad. A Mac joining asks you to approve it right here; an iPhone or iPad joins "
|
||||||
|
+ "on its own.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user