feat(apple): offer to remember a device after a transfer

Closes the loop: until now nothing in the UI could create a contact, so the
list stayed empty unless the peer initiated.

A completed receive names its sender and a completed delivery names its
receiver, so both sides get the suggestion. Declining is persisted, or every
later transfer with the same device would re-ask a question already
answered; pairing deliberately afterwards clears that.

The suggestion sheet ranks below the two other prompts, since nobody is
waiting on the answer.
This commit is contained in:
2026-08-06 18:06:19 +02:00
parent 1369df4578
commit dc23e87c56
5 changed files with 368 additions and 4 deletions

View File

@@ -124,6 +124,9 @@ struct AppPreferences: Equatable {
var relayConfiguration: RelayConfiguration
/// Idle lifetime applied to grants this device issues from now on.
var grantLifetime: GrantLifetimeOption
/// Devices the user declined to remember. Persisted so a repeat transfer
/// with the same device does not re-ask forever.
var declinedPairingSuggestions: Set<String>
}
struct AppPreferencesDefaults {
@@ -148,6 +151,7 @@ final class AppPreferencesRepository: ObservableObject {
static let diagnosticsInstallId = "diagnostics_install_id"
static let relayConfiguration = "relay_configuration"
static let grantLifetime = "grant_lifetime"
static let declinedPairingSuggestions = "declined_pairing_suggestions"
}
init(defaults: UserDefaults = .standard, fallback: AppPreferencesDefaults) {
@@ -163,13 +167,15 @@ final class AppPreferencesRepository: ObservableObject {
let installId = defaults.string(forKey: Key.diagnosticsInstallId) ?? ""
let grantLifetime = defaults.string(forKey: Key.grantLifetime)
.flatMap(GrantLifetimeOption.init(rawValue:)) ?? .days90
let declined = Set(defaults.stringArray(forKey: Key.declinedPairingSuggestions) ?? [])
return AppPreferences(
username: username,
receiveFolder: folder,
themeMode: themeMode,
diagnosticsInstallId: installId,
relayConfiguration: resolveRelayConfiguration(defaults),
grantLifetime: grantLifetime
grantLifetime: grantLifetime,
declinedPairingSuggestions: declined
)
}
@@ -215,6 +221,22 @@ final class AppPreferencesRepository: ObservableObject {
setReceiveFolder(fallback.receiveFolder)
}
func declinePairingSuggestion(_ endpointId: String) {
var declined = preferences.declinedPairingSuggestions
declined.insert(endpointId)
defaults.set(Array(declined), forKey: Key.declinedPairingSuggestions)
reload()
}
/// Clears the decline so the device can be suggested again, used when the
/// user pairs with it deliberately.
func clearDeclinedPairingSuggestion(_ endpointId: String) {
var declined = preferences.declinedPairingSuggestions
guard declined.remove(endpointId) != nil else { return }
defaults.set(Array(declined), forKey: Key.declinedPairingSuggestions)
reload()
}
func setGrantLifetime(_ lifetime: GrantLifetimeOption) {
defaults.set(lifetime.rawValue, forKey: Key.grantLifetime)
reload()