fix(apple): show receiver-approval modal on macOS release builds

The approval modal never appeared for a macOS sender: the receiver request
reached the core and even fired its notification, but the modal stayed hidden.

Root cause was observation, not presentation. `RootView` derived `approvals`
and `messages` as `@ObservedObject` in `init` from a freshly built `AppGraph`.
`init` runs on every view re-creation and each run makes a throwaway graph, so
those observed objects were repointed to a dead `ApprovalCoordinator` that never
receives core events — while the persisted `@StateObject graph` (and the models
wired to it) kept the live one. Debug happened not to re-init the view, so it
stayed on the live instance; release re-inits it, exposing the bug.

Move the snackbar + approval modal into an `OverlayLayer` child view that takes
the coordinator/messages as `@ObservedObject` and is constructed in `body` from
the persisted `graph`, so the subscription is always against the live instances.

While here:
- Present the approval only after any open share/QR sheet has actually finished
  dismissing (macOS can't stack sheets), driven off the sheet's real
  `onDismiss` completion via a new `AdaptiveDrawer.onDismissed` hook and
  `SendModel.shareSheetsDismissed` — no wall-clock delay.
- Move the list-level share-sheet state (`shareTargetId`) into `SendModel` so the
  approval flow can dismiss every share surface centrally.
- Add a fallback: pending receiver rows in the Receivers panel now offer an
  Approve action (`SendModel.acceptReceiver`) alongside Refuse, for the case the
  modal didn't surface.
This commit is contained in:
2026-07-31 11:19:28 +02:00
parent 56d19014d4
commit 5424da855e
5 changed files with 162 additions and 53 deletions

View File

@@ -7,14 +7,18 @@ struct SendScreen: View {
@ObservedObject var model: SendModel
let windowClass: WindowClass
/// Transfer whose share panel is presented inline from the list context menu.
@State private var shareTarget: Transfer?
/// Transfer pending an inline (list-level) delete confirmation.
@State private var deleteTarget: Transfer?
private var outgoing: [Transfer] {
model.coreState.transfers.filter { $0.direction == .send }
}
/// The transfer whose list-level share sheet is open, resolved from the model's
/// `shareTargetId` (kept in the model so the approval flow can dismiss it).
private var shareTarget: Transfer? {
guard let id = model.state.shareTargetId else { return nil }
return outgoing.first { $0.transferId == id }
}
private var selectedTransfer: Transfer? {
guard let id = model.state.selectedTransferId else { return nil }
return outgoing.first { $0.transferId == id }
@@ -50,9 +54,10 @@ struct SendScreen: View {
// composer drawer on the outer body, so the two don't clash). Opens the
// share panel over the list without navigating into the transfer detail.
.adaptiveDrawer(
isPresented: Binding(get: { shareTarget != nil }, set: { if !$0 { shareTarget = nil } }),
isPresented: Binding(get: { shareTarget != nil }, set: { if !$0 { model.closeShareTarget() } }),
windowClass: windowClass,
onDismiss: { shareTarget = nil }
onDismiss: model.closeShareTarget,
onDismissed: model.shareSheetDidDismiss
) {
if let shareTarget {
TransferSharePanel(model: model, transfer: shareTarget)
@@ -92,7 +97,8 @@ struct SendScreen: View {
.adaptiveDrawer(
isPresented: Binding(get: { model.state.detailPanel != nil }, set: { _ in }),
windowClass: windowClass,
onDismiss: model.closeDetailPanel
onDismiss: model.closeDetailPanel,
onDismissed: model.shareSheetDidDismiss
) {
if let panel = model.state.detailPanel {
DetailPanelContent(model: model, transfer: transfer, panel: panel)
@@ -127,7 +133,7 @@ struct SendScreen: View {
.contextMenu {
if transfer.ticket != nil {
Button {
shareTarget = transfer
model.openShareTarget(transfer.transferId)
} label: {
Label(String(localized: L10n.Transfer.shareTitle), systemSymbol: .squareAndArrowUp)
}