import LChatCore import SwiftUI // 🤖 Spaces ▸ Add from Link…: paste any Eleven invite link and the space joins // this Mac NATIVELY — SpaceClaim handles all three link shapes (member/setup // invites, DM invites, add-device links) with no webview; the secret in the // #fragment never reaches any server. addInstance persists the identity in // the Keychain, selects the space, and pushes it to the member's Eleven // account (home vault), so it follows them to every signed-in device. struct AddFromLinkSheet: View { @EnvironmentObject var app: MacAppState @Environment(\.dismiss) private var dismiss @State private var link = "" @State private var busy = false @State private var error: String? @State private var hint: String? /// The browser handoff's pre-fill (onOpenURL stashes the validated link; /// see LChatMacApp): consumed once, so a later manual open starts blank. /// The locked pane's rungs stash contextual guidance the same way. private func consumePendingLink() { if let pending = app.pendingAddLink { link = pending app.pendingAddLink = nil } if let h = app.addFromLinkHint { hint = h app.addFromLinkHint = nil } } var body: some View { VStack(alignment: .leading, spacing: 12) { Text("Add a space from a link") .font(.headline) Text(hint ?? "Paste an invite link — the space joins this Mac and syncs to your Eleven account.") .font(.callout) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) TextField("https://…/join/…", text: $link) .textFieldStyle(.roundedBorder) .disabled(busy) .onSubmit(add) if let error { Text(error) .font(.footnote) .foregroundStyle(.red) .fixedSize(horizontal: false, vertical: true) } HStack { Button("Cancel") { dismiss() } .keyboardShortcut(.cancelAction) .disabled(busy) Spacer() Button { add() } label: { HStack(spacing: 6) { if busy { ProgressView().controlSize(.small) } Text(busy ? "Joining…" : "Add Space") } } .buttonStyle(.borderedProminent) .keyboardShortcut(.defaultAction) .disabled(busy || parseJoinLink(link.trimmingCharacters(in: .whitespacesAndNewlines)) == nil) } } .padding(20) .frame(width: 440) .onAppear(perform: consumePendingLink) // A handoff can land while the sheet is already up (a second click // in the browser) — take the newest link in place. .onChange(of: app.pendingAddLink) { _, pending in if pending != nil, !busy { consumePendingLink() } } } private func add() { let trimmed = link.trimmingCharacters(in: .whitespacesAndNewlines) guard !busy, parseJoinLink(trimmed) != nil else { if !trimmed.isEmpty { error = "That doesn't look like an Eleven invite link." } return } busy = true error = nil Task { @MainActor in do { // Carry the name you already go by; an add-device link's own // "~name" (or "Me") wins inside the claim when this is empty. let screenName = app.instancesInRailOrder().first(where: { !$0.isArchived })?.screenName ?? app.instances.first?.screenName ?? "" let inst = try await SpaceClaim.claim(setupLink: trimmed, screenName: screenName) app.addInstance(inst) // Keychain + rail + select + vault push dismiss() } catch { self.error = (error as? LocalizedError)?.errorDescription ?? "Couldn't add that space." busy = false } } } }