eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1import LChatCore
2import SwiftUI
3
4// 🤖 Invite a brand-new person straight into a DM (the web's inviteSomeone,
5// natively): type their name, get a join link to share. The link's #fragment
6// carries the one-time secret — it never touches the server, so share it
7// over any channel you trust.
8struct InviteSomeoneView: View {
9 @ObservedObject var store: ThreadStore
10 let onCreated: (String?) -> Void
11
12 @State private var name = ""
13 @State private var busy = false
14 @State private var error: String?
15 @State private var link: String?
16 @State private var copied = false
17
18 var body: some View {
19 Group {
20 if let link {
21 ready(link: link)
22 } else {
23 form
24 }
25 }
26 .navigationTitle("Invite someone")
27 .navigationBarTitleDisplayMode(.inline)
28 }
29
30 // Step 1: their name + a PROMINENT create button (a bare button row in a
31 // List reads as another text field — the lesson from the first device run).
32 private var form: some View {
33 VStack(spacing: 16) {
34 Form {
35 Section {
36 TextField("Their name", text: $name)
37 .textInputAutocapitalization(.words)
38 .onSubmit(create)
39 } footer: {
40 Text("Creates a pending chat with them and a join link only they can use.")
41 }
42 if let error {
43 Section { Text(error).font(.footnote).foregroundStyle(.red) }
44 }
45 }
46 Button {
47 create()
48 } label: {
49 HStack(spacing: 8) {
50 if busy { ProgressView().tint(.white) }
51 Text(busy ? "Creating…" : "Create invite")
52 .font(.body.weight(.semibold))
53 }
54 .frame(maxWidth: .infinity)
55 .padding(.vertical, 4)
56 }
57 .buttonStyle(.borderedProminent)
58 .disabled(busy || name.trimmingCharacters(in: .whitespaces).isEmpty)
59 .padding(.horizontal, 20)
60 .padding(.bottom, 16)
61 }
62 .background(Color(.systemGroupedBackground))
63 }
64
65 // Step 2: the link itself, Copy, and the system share sheet. The content
66 // scrolls (large Dynamic Type / small screens); the buttons pin below it.
67 private func ready(link: String) -> some View {
68 ScrollView {
69 VStack(spacing: 18) {
70 Image(systemName: "person.badge.plus")
71 .font(.system(size: 40))
72 .foregroundStyle(Color.accentColor)
73 .padding(.top, 24)
74 Text("Invite ready")
75 .font(.title3.weight(.semibold))
76 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.")
77 .font(.callout)
78 .foregroundStyle(.secondary)
79 .multilineTextAlignment(.center)
80 .padding(.horizontal, 24)
81 Text(link)
82 .font(.footnote.monospaced())
83 .lineLimit(3)
84 .truncationMode(.middle)
85 .textSelection(.enabled)
86 .padding(12)
87 .frame(maxWidth: .infinity)
88 .background(Color(.secondarySystemGroupedBackground),
89 in: RoundedRectangle(cornerRadius: 10))
90 .padding(.horizontal, 20)
91 }
92 }
93 .safeAreaInset(edge: .bottom) {
94 VStack(spacing: 10) {
95 if let url = URL(string: link) {
96 ShareLink(item: url) {
97 Label("Share invite link", systemImage: "square.and.arrow.up")
98 .font(.body.weight(.semibold))
99 .frame(maxWidth: .infinity)
100 .padding(.vertical, 4)
101 }
102 .buttonStyle(.borderedProminent)
103 }
104 Button {
105 UIPasteboard.general.string = link
106 copied = true
107 } label: {
108 Label(copied ? "Copied" : "Copy link",
109 systemImage: copied ? "checkmark" : "doc.on.doc")
110 .frame(maxWidth: .infinity)
111 .padding(.vertical, 4)
112 }
113 .buttonStyle(.bordered)
114 }
115 .padding(.horizontal, 20)
116 .padding(.top, 10)
117 .padding(.bottom, 16)
118 .background(Color(.systemGroupedBackground))
119 }
120 .background(Color(.systemGroupedBackground))
121 }
122
123 private func create() {
124 let trimmed = name.trimmingCharacters(in: .whitespaces)
125 guard !trimmed.isEmpty, !busy else { return }
126 busy = true
127 error = nil
128 Task {
129 let r = await store.inviteSomeone(name: trimmed)
130 busy = false
131 if let l = r.link {
132 link = l
133 } else {
134 error = r.error ?? "Couldn't create the invite."
135 }
136 }
137 }
138}
139