mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-07 11:19:58 +02:00
Two causes. The connect step had no timeout, so an unreachable device was retried indefinitely instead of falling through to hold-for-later; it now gives up after 15s and holds the offer as designed. The picker also waited on the whole exchange, which includes a person on the other device deciding — up to two minutes. It now closes on tap and reports the outcome as a message, and a decline or an unanswered offer is shown as information rather than an error, since the offer did arrive.
74 lines
2.1 KiB
Swift
74 lines
2.1 KiB
Swift
import SFSafeSymbols
|
|
import SwiftUI
|
|
|
|
/// Picks a remembered device to send an existing transfer to.
|
|
///
|
|
/// Offered next to the QR code as another way to deliver the same invitation,
|
|
/// not as a second share of the same files.
|
|
struct DevicePickerSheet: View {
|
|
@ObservedObject var model: ContactsModel
|
|
let transferId: UInt64
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
/// Only devices holding a live grant: the rest cannot be reached until they
|
|
/// are paired again, so offering them here would fail on tap.
|
|
private var reachable: [DeviceContact] {
|
|
model.state.contacts.filter(\.canSend)
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Group {
|
|
if reachable.isEmpty {
|
|
ContentUnavailableView {
|
|
Label(
|
|
String(localized: L10n.Contacts.pickDeviceTitle),
|
|
systemSymbol: .macbookAndIphone
|
|
)
|
|
} description: {
|
|
Text(String(localized: L10n.Contacts.pickDeviceEmpty))
|
|
}
|
|
} else {
|
|
List(reachable) { contact in
|
|
Button {
|
|
// Close first. The other device's user has to accept,
|
|
// which can take as long as they take, and holding a
|
|
// modal open on someone else's decision reads as a
|
|
// hang. The outcome arrives as a message instead.
|
|
dismiss()
|
|
model.offerTransferInBackground(transferId: transferId, to: contact)
|
|
} label: {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(contact.displayName)
|
|
Text(contact.shortFingerprint)
|
|
.font(.caption.monospaced())
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Spacer()
|
|
if model.state.busyEndpoints.contains(contact.endpointId) {
|
|
ProgressView().controlSize(.small)
|
|
}
|
|
}
|
|
}
|
|
.disabled(model.state.busyEndpoints.contains(contact.endpointId))
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle(Text(String(localized: L10n.Contacts.pickDeviceTitle)))
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
#endif
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button(String(localized: L10n.Button.cancel)) { dismiss() }
|
|
}
|
|
}
|
|
}
|
|
.task { await model.refresh() }
|
|
#if os(macOS)
|
|
.frame(minWidth: 380, minHeight: 320)
|
|
#endif
|
|
}
|
|
}
|