import RastrilloApple import SwiftUI @MainActor struct DocsSharingView: View { let client: NativeHTTP let sqid: String @Environment(\.dismiss) private var dismiss @State private var information: DocsShareInformation? @State private var address = "" @State private var role = "reader" @State private var busy = false @State private var needsRefresh = true @State private var errorKey: String? @State private var oneTimeURL: URL? var body: some View { NavigationStack { Form { if let errorKey { Section { Text(LocalizedStringKey(errorKey), bundle: DocsResources.bundle) Button { Task { await refresh() } } label: { label("native.retry") } .disabled(busy) } } if busy { ProgressView() } if let information { Section { if information.grants.isEmpty { label("docs.share.empty") } ForEach(information.grants) { grant in VStack(alignment: .leading) { Text(verbatim: grant.address).textSelection(.enabled) label(grant.role == "writer" ? "docs.share.role.writer" : "docs.share.role.reader") .foregroundStyle(.secondary) Button(role: .destructive) { Task { await change("/revoke", values: ["address": grant.address]) } } label: { label("docs.share.revoke") } .disabled(busy || needsRefresh) } } } header: { label("docs.share.heading") } Section { TextField(String(localized: "docs.share.address", bundle: DocsResources.bundle), text: $address) .autocorrectionDisabled() #if os(iOS) .textInputAutocapitalization(.never) .keyboardType(.emailAddress) #endif Picker(selection: $role) { label("docs.share.role.reader").tag("reader") label("docs.share.role.writer").tag("writer") } label: { label("docs.share.role") } Button { Task { await change("", values: ["address": address, "role": role]) } } label: { label("docs.share.submit") } .disabled(busy || needsRefresh || address.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) }.disabled(busy) Section { label("docs.share.public.body") if let oneTimeURL { Text(verbatim: oneTimeURL.absoluteString).textSelection(.enabled) ShareLink(item: oneTimeURL) { label("docs.share.submit") } label("docs.share.public.once").foregroundStyle(.secondary) } else if information.published { label("docs.share.public.published").foregroundStyle(.secondary) } if information.published { Button { Task { await change("/link", values: ["on": "1"]) } } label: { label("docs.share.public.replace") } Button(role: .destructive) { Task { await change("/link", values: ["on": "0"]) } } label: { label("docs.share.public.off") } } else { Button { Task { await change("/link", values: ["on": "1"]) } } label: { label("docs.share.public.on") } } } header: { label("docs.share.public.heading") } footer: { label("docs.share.public.hint") } .disabled(busy || needsRefresh) } } .formStyle(.grouped) .navigationTitle(Text("docs.share.title", bundle: DocsResources.bundle)) .toolbar { ToolbarItem(placement: .confirmationAction) { Button { dismiss() } label: { label("docs.share.back") }.disabled(busy) } } .task(id: sqid) { await refresh() } .interactiveDismissDisabled(busy) #if os(macOS) .frame(minWidth: 460, idealWidth: 540, minHeight: 520) #endif } } private func label(_ key: String) -> Text { Text(LocalizedStringKey(key), bundle: DocsResources.bundle) } private func load() async throws { let state: DocsShareInformation = try await client.request("/d/\(sqid)/share") information = state if !state.published { oneTimeURL = nil } else if !state.public_url.isEmpty { oneTimeURL = URL(string: state.public_url) } needsRefresh = false } private func refresh() async { guard !busy else { return } busy = true defer { busy = false } errorKey = nil do { try await load() } catch { needsRefresh = true; errorKey = "native.share.loadFailed" } } private func change(_ suffix: String, values: [String: String]) async { guard !busy && !needsRefresh else { return } busy = true errorKey = nil defer { busy = false } // URL query encoding leaves '+' literal; forms interpret it as space. var components = URLComponents() components.queryItems = values.sorted { $0.key < $1.key }.map { URLQueryItem(name: $0.key, value: $0.value) } let body = Data((components.percentEncodedQuery ?? "").replacingOccurrences(of: "+", with: "%2B").utf8) do { let result: DocsShareMutation = try await client.request("/d/\(sqid)/share\(suffix)", method: "POST", body: body, contentType: "application/x-www-form-urlencoded") if suffix == "/link" { guard let published = result.published else { throw NativeClientError.message("Invalid sharing response") } oneTimeURL = published ? result.public_url.flatMap(URL.init(string:)) : nil } else { guard result.ok == true else { throw NativeClientError.message("Invalid sharing response") } if suffix.isEmpty { address = "" } } do { try await load() } catch { needsRefresh = true; errorKey = "native.share.loadFailed" } } catch { needsRefresh = true; errorKey = "native.share.saveFailed" } } }