import LChatCore import SwiftUI // 🤖 Invite a brand-new person straight into a DM (the web's inviteSomeone, // natively): type their name, get a join link to share. The link's #fragment // carries the one-time secret — it never touches the server, so share it // over any channel you trust. struct InviteSomeoneView: View { @ObservedObject var store: ThreadStore let onCreated: (String?) -> Void @State private var name = "" @State private var busy = false @State private var error: String? @State private var link: String? @State private var copied = false var body: some View { Group { if let link { ready(link: link) } else { form } } .navigationTitle("Invite someone") .navigationBarTitleDisplayMode(.inline) } // Step 1: their name + a PROMINENT create button (a bare button row in a // List reads as another text field — the lesson from the first device run). private var form: some View { VStack(spacing: 16) { Form { Section { TextField("Their name", text: $name) .textInputAutocapitalization(.words) .onSubmit(create) } footer: { Text("Creates a pending chat with them and a join link only they can use.") } if let error { Section { Text(error).font(.footnote).foregroundStyle(.red) } } } Button { create() } label: { HStack(spacing: 8) { if busy { ProgressView().tint(.white) } Text(busy ? "Creating…" : "Create invite") .font(.body.weight(.semibold)) } .frame(maxWidth: .infinity) .padding(.vertical, 4) } .buttonStyle(.borderedProminent) .disabled(busy || name.trimmingCharacters(in: .whitespaces).isEmpty) .padding(.horizontal, 20) .padding(.bottom, 16) } .background(Color(.systemGroupedBackground)) } // Step 2: the link itself, Copy, and the system share sheet. The content // scrolls (large Dynamic Type / small screens); the buttons pin below it. private func ready(link: String) -> some View { ScrollView { VStack(spacing: 18) { Image(systemName: "person.badge.plus") .font(.system(size: 40)) .foregroundStyle(Color.accentColor) .padding(.top, 24) Text("Invite ready") .font(.title3.weight(.semibold)) Text("Send this link to \(name.trimmingCharacters(in: .whitespaces)) — it's their one-time key into your chat. A new one can be minted later if it's lost.") .font(.callout) .foregroundStyle(.secondary) .multilineTextAlignment(.center) .padding(.horizontal, 24) Text(link) .font(.footnote.monospaced()) .lineLimit(3) .truncationMode(.middle) .textSelection(.enabled) .padding(12) .frame(maxWidth: .infinity) .background(Color(.secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: 10)) .padding(.horizontal, 20) } } .safeAreaInset(edge: .bottom) { VStack(spacing: 10) { if let url = URL(string: link) { ShareLink(item: url) { Label("Share invite link", systemImage: "square.and.arrow.up") .font(.body.weight(.semibold)) .frame(maxWidth: .infinity) .padding(.vertical, 4) } .buttonStyle(.borderedProminent) } Button { UIPasteboard.general.string = link copied = true } label: { Label(copied ? "Copied" : "Copy link", systemImage: copied ? "checkmark" : "doc.on.doc") .frame(maxWidth: .infinity) .padding(.vertical, 4) } .buttonStyle(.bordered) } .padding(.horizontal, 20) .padding(.top, 10) .padding(.bottom, 16) .background(Color(.systemGroupedBackground)) } .background(Color(.systemGroupedBackground)) } private func create() { let trimmed = name.trimmingCharacters(in: .whitespaces) guard !trimmed.isEmpty, !busy else { return } busy = true error = nil Task { let r = await store.inviteSomeone(name: trimmed) busy = false if let l = r.link { link = l } else { error = r.error ?? "Couldn't create the invite." } } } }