TeraKeyRemovalView.swift (4611B)
1 import SwiftUI 2 3 enum TeraKeyRemovalConsent { 4 static let local = "This removes only this device’s signing key. Drafts, photos, settings, signed requests and receipts remain on this device. This does not erase local data." 5 static let remote = "Published copies may remain with relays and other people. Key removal does not retract them. To request retraction, use Retract on each post first, then select its saved request below. Linked revisions must finish in Drafts & outbox." 6 static let recovery = "Selected requests are signed and saved before removal; delivery is not guaranteed. The app stops before removing the key. To open this account’s retained data or resume delivery later, you must import the same key from your own backup. Unselected unsigned work cannot be signed without that key." 7 } 8 9 struct TeraKeyRemovalView: View { 10 let author: String 11 @EnvironmentObject private var appModel: TeraAppModel 12 @Environment(\.dismiss) private var dismiss 13 @State private var requests: [TeraLegacyDraftSummary] = [] 14 @State private var selected: Set<String> = [] 15 @State private var cursor: String? 16 @State private var loaded = false 17 @State private var loading = false 18 @State private var consent = false 19 @State private var message: String? 20 @State private var removal: Task<Void, Never>? 21 22 var body: some View { 23 NavigationStack { 24 List { 25 Section("Local signing key") { 26 Text(TeraKeyRemovalConsent.local) 27 Text(author).font(.caption.monospaced()) 28 } 29 Section("Remote deletion requests") { 30 Text(TeraKeyRemovalConsent.remote) 31 ForEach(requests) { request in 32 Toggle(isOn: Binding(get: { selected.contains(request.id) }, set: { enabled in 33 if enabled { 34 selected.insert(request.id) 35 } else { 36 selected.remove(request.id) 37 } 38 })) { 39 VStack(alignment: .leading) { 40 Text("Saved retraction \(request.id.prefix(8))").font(.caption.monospaced()) 41 Text(request.honestSummary).font(.caption) 42 } 43 } 44 } 45 if loaded, requests.isEmpty { 46 Text("No independent saved retraction requests are shown.") 47 } 48 if cursor != nil { 49 Button("Next page of requests") { Task { await load() } } 50 .disabled(!selected.isEmpty || loading) 51 Text("Selections apply to this page only. Prepare other requests in Drafts & outbox before removal.").font(.caption) 52 } 53 if loading { 54 ProgressView() 55 } 56 if let message { 57 Text(message) 58 } 59 } 60 Section("Before removal") { 61 Text(TeraKeyRemovalConsent.recovery) 62 Toggle("I understand the retained data and loss of signing access", isOn: $consent) 63 .accessibilityIdentifier("tera.keyRemoval.consent") 64 Button("Remove this signing key", role: .destructive) { 65 let chosen = requests.filter { selected.contains($0.id) }.map { 66 TeraKeyRemovalRequest(id: $0.id, revision: $0.revision) 67 } 68 removal = Task { 69 await appModel.removeSigningKey(author: author, requests: chosen) 70 removal = nil 71 dismiss() 72 } 73 } 74 .disabled(!consent || !loaded || loading || removal != nil) 75 .accessibilityIdentifier("tera.keyRemoval.confirm") 76 } 77 } 78 .disabled(removal != nil) 79 .navigationTitle("Remove signing key") 80 .toolbar { 81 ToolbarItem(placement: .cancellationAction) { 82 Button("Cancel") { removal?.cancel(); dismiss() } 83 } 84 } 85 .task { 86 if !loaded { 87 await load() 88 } 89 } 90 .onDisappear { removal?.cancel() } 91 .interactiveDismissDisabled(removal != nil) 92 } 93 } 94 95 private func load() async { 96 guard !loading else { return } 97 loading = true 98 defer { loading = false } 99 do { 100 let page = try await appModel.keyRemovalPage(author: author, cursor: cursor) 101 try Task.checkCancellation() 102 requests = page.entries.compactMap { entry in 103 guard case let .draft(draft) = entry, draft.kind == .retraction, 104 draft.revisionParentID == nil else { return nil } 105 return draft 106 } 107 selected.removeAll() 108 cursor = page.nextCursor 109 loaded = true 110 message = nil 111 } catch { 112 loaded = false 113 message = "Saved requests could not be checked. Cancel and retry before removing the key." 114 } 115 } 116 }