eleven / ocho Public

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

Plain git — no account needed to clone.

Download

Download this file

1// The joiner welcome conversation as DATA (docs/conversation-scripts.md) β€”
2// Eleven greeting a new member: passkey FIRST (so the ceremony's round-trip
3// lands mid-conversation, not on "all done"), then name, profile photo, and
4// the finish chips. The engine (flow.js) interprets it; welcome.js binds it to
5// the host bridge. JSON-pure (test-enforced): this is the document the server
6// will ship to native interpreters.
7export default {
8 schema: 1,
9 flow: "welcome",
10 entry: "hello",
11 resumedIf: { any: ["done.access", "done.name", "bridge.myName"] },
12 // The passkey RETURN leg gets its own richer ack in the access step's entry,
13 // so it skips the generic re-greeting.
14 resumeUnless: { all: ["bridge.enrolled", "!done.access"] },
15 resumeSay: "Welcome back β€” let's pick up where we left off.",
16 steps: [
17 {
18 id: "hello",
19 skipIf: ["resumed"],
20 // Assume nothing: say what this place IS, who runs it, and what the
21 // encryption means for them β€” in lock-and-key terms, not jargon.
22 bubbles: [
23 {
24 text: [
25 "Welcome! πŸ‘‹ I'm Eleven, your guide here.\n\n",
26 { if: "inviterName", text: "{inviterName} invited you to {space.name|this space}" },
27 { if: "!inviterName", text: "You've been invited to {space.name|this space}" },
28 " β€” a private, invite-only chat space. It isn't a big platform with accounts and feeds: it's a small server ",
29 { if: "adminName", text: "run by {adminName}" },
30 { if: "!adminName", text: "run by one of its members" },
31 ", just for the people in it.\n\n",
32 "Everything here β€” messages, photos, even names β€” is locked with keys only members hold, so nobody outside a chat can ever read it. Not the server, and not whoever runs it.",
33 ],
34 },
35 ],
36 auto: "access",
37 },
38 {
39 id: "access",
40 entries: [
41 {
42 // Native shells (iOS/Mac) WITH a live Eleven session: access is
43 // already secured by that sign-in β€” the app pushed this space into
44 // its vault as it joined β€” so the ask is moot.
45 if: { all: ["native", "bridge.homeSignedIn"] },
46 recordIfNew: { label: "Your access is already secured by your Eleven sign-in", value: "native" },
47 goto: "name",
48 },
49 {
50 if: { any: ["bridge.enrolled", "done"] },
51 recordIfNew: { label: "Signed in β€” your access is safe", value: "passkey" },
52 // Just back from the sign-in ceremony: acknowledge it worked, then
53 // move on β€” a separate bubble, so the ack lands before the question.
54 // Covers BOTH arrivals: the mid-conversation return leg AND a joiner
55 // who signed up before the conversation started (signup-first join) β€”
56 // so no "welcome back", which reads wrong when nobody left.
57 sayIfNew: "Great β€” you're signed in, so your access is safe. πŸ”’ Let's continue.",
58 goto: "name",
59 },
60 // Nowhere to sign in: a self-hosted space with no home server (#1377),
61 // or a native host that wired no sign-in. The ask would be a dead-end
62 // chip β€” record where the access actually lives and carry on.
63 {
64 if: "!bridge.canEnrol",
65 recordIfNew: {
66 label: { if: "native", then: "Your access lives on this device", else: "Your access lives in this browser" },
67 value: "local",
68 },
69 goto: "name",
70 },
71 ],
72 // The sign-in returns asynchronously (a redirect back, tabbing back from
73 // a system browser, or a native sheet closing); auto-detect it on either
74 // signal and carry on without a manual "Continue" tap.
75 settle: { when: { any: ["bridge.enrolled", "bridge.homeSignedIn"] } },
76 // One bubble, chips attached β€” context and ask together, so the opening
77 // run is two messages (intro + this), not three. The member-facing word
78 // is "sign in" β€” never the mechanism (passkeys, backups).
79 bubbles: [
80 {
81 // Segments, so only the noun changes between surfaces (see the
82 // setup flow's keys step).
83 text: [
84 "Before anything else, let's make sure you can't lose your way in. πŸ”‘\n\n",
85 { if: "native", text: "Your access to this space lives in a key that exists only on this device β€” there's no username or password, and nobody can reset it for you. If you lose the device, your access goes with it.\n\n" },
86 { if: "!native", text: "Your access to this space lives in a key that exists only in this browser β€” there's no username or password, and nobody can reset it for you. If this browser's data is cleared, your access goes with it.\n\n" },
87 { if: "native", text: "Sign in with Eleven and your access is safe β€” it's saved to your account, and every device you sign in on can open this space." },
88 { if: "!native", text: "Sign in with Eleven and your access is safe β€” one tap, your face or fingerprint, and you'll be brought straight back so we can carry on." },
89 ],
90 },
91 ],
92 chips: [
93 {
94 label: "Sign in (recommended)",
95 advancing: false,
96 effect: { verb: "savePasskey" },
97 probe: { afterMs: 1500, unless: { any: ["bridge.enrolled", "bridge.homeSignedIn"] }, goto: "access-waiting" },
98 },
99 {
100 label: "Not now",
101 spent: "Skipped",
102 effect: { verb: "deferBackupReminders" },
103 record: { label: "Skipped signing in for now", value: "skipped" },
104 ack: "No problem β€” I'll nudge you again in a day or so.",
105 goto: "name",
106 },
107 // The fast path for people in a hurry: one form with every question at
108 // once, ending in the same sign-in. Lives on this step deliberately β€”
109 // its settle (bridge.enrolled) is what carries the flow on after the
110 // form's fast-filled records + sign-in hand-off.
111 {
112 label: "Fill in a form instead",
113 trailing: true,
114 advancing: false,
115 effect: { verb: "openQuickForm" },
116 },
117 ],
118 },
119 {
120 id: "access-waiting",
121 // The ceremony may confirm while we sit here (the completion poll flips
122 // the flag) β€” re-enter access so its entry records + acks, no tap needed.
123 settle: { when: { any: ["bridge.enrolled", "bridge.homeSignedIn"] }, goto: "access" },
124 bubbles: [{ text: "I'll wait here while you finish signing in. When you're done, tap Continue." }],
125 chips: [
126 {
127 label: "Continue",
128 record: {
129 step: "access",
130 label: {
131 if: { any: ["bridge.enrolled", "bridge.homeSignedIn"] },
132 then: "Signed in β€” your access is safe",
133 else: "Signed in (in this browser)",
134 },
135 value: "passkey",
136 },
137 ack: "Perfect β€” you're all set. πŸ”’",
138 goto: "name",
139 },
140 {
141 label: "It didn't work β€” skip for now",
142 spent: "Skipped",
143 effect: { verb: "deferBackupReminders" },
144 record: { step: "access", label: "Skipped signing in for now", value: "skipped" },
145 ack: "No problem β€” I'll nudge you again in a day or so.",
146 goto: "name",
147 },
148 ],
149 },
150 {
151 id: "name",
152 skipIf: ["done", "bridge.myName"],
153 skipTo: "photo", // past name-email, the confirm step (see setup.flow.js)
154 bubbles: [
155 {
156 text: "{ack?}First things first β€” what should folks here call you? Reply with your name.",
157 key: "q-name",
158 label: "Your name:",
159 hint: "e.g. Finn Wolfhard",
160 },
161 ],
162 chips: [
163 {
164 if: "suggested",
165 label: "I'm {suggested}",
166 set: { "vars.answer": "{suggested}" },
167 effect: { verb: "setMyName", args: ["$answer"] },
168 record: { label: "noted: you're {vars.answer}", value: "$answer" },
169 ack: "Lovely to meet you, {vars.answer}! 🀝",
170 goto: "photo",
171 },
172 ],
173 // Same guard as setup's name step: an email typed here would become the
174 // name everyone in the space sees (flow.js input.confirm).
175 input: { kind: "text", required: true, maxLen: 32, clean: "name", confirm: { when: "email", goto: "name-email" } },
176 effect: { verb: "setMyName", args: ["$answer"] },
177 on: {
178 ok: {
179 record: { label: "noted: you're {vars.answer}", value: "$answer" },
180 ack: "Lovely to meet you, {vars.answer}! 🀝",
181 goto: "photo",
182 },
183 },
184 },
185 {
186 id: "name-email",
187 bubbles: [
188 {
189 text: "That's an email address β€” is that what you'd like to be called here? It's the name everyone in this space sees.",
190 key: "q-name-email",
191 },
192 ],
193 chips: [
194 {
195 label: "Use it anyway",
196 spent: "Kept",
197 effect: { verb: "setMyName", args: ["$answer"] },
198 record: { step: "name", label: "noted: you're {vars.answer}", value: "$answer" },
199 ack: "Righto β€” {vars.answer} it is. 🀝",
200 goto: "photo",
201 },
202 { label: "Type a different name", spent: "Changed", goto: "name" },
203 ],
204 },
205 {
206 id: "photo",
207 // Native v1: no attachment flow in the conversation composer β€” the
208 // profile photo lives in your profile there.
209 skipIf: ["done", "native"],
210 skipTo: "finish",
211 bubbles: [
212 {
213 text: "{ack?}Want to add a profile photo? Only members of this space can see it β€” like everything else here, it's encrypted.",
214 key: "q-photo",
215 },
216 {
217 text: "Send me a photo β€” tap the + next to the message box, or just drag a picture into the chat, and hit send. If you type a message with it, I'll keep that as the photo's description. Or skip for now.",
218 attachHint: "Optional: describe your photo",
219 },
220 ],
221 chips: [
222 {
223 label: "Skip for now",
224 spent: "Skipped",
225 cancelInput: true,
226 set: { "vars.skippedPhoto": true },
227 // Recorded so a resume (notably the passkey return leg) doesn't
228 // re-ask a step the person already waved away. A skip is an answer.
229 record: { label: "skipped the profile photo", value: "skipped" },
230 goto: "finish",
231 },
232 ],
233 input: {
234 kind: "photo",
235 reject: { text: "Send it as a picture β€” tap the + next to the message box or drag one in β€” or tap Skip.", local: true },
236 },
237 effect: { verb: "adoptProfilePhoto" },
238 on: {
239 ok: { goto: "photo-preview" },
240 nokey: {
241 say: "One sec β€” I'm still finishing your secure setup. Try sending it again in a moment, or skip for now.",
242 local: true,
243 ask: "Ready when you are β€” send that photo again, or skip it.",
244 },
245 error: {
246 say: "Hmm, I couldn't use that one β€” try another?",
247 local: true,
248 ask: "Of course! Send me another one whenever you're ready β€” and pop a message with it if you'd like a description. πŸ“·",
249 },
250 },
251 },
252 {
253 id: "photo-preview",
254 // The preview is a client-only notice: the photo is an encrypted blob,
255 // so its decrypted object URL can't ride a persisted event.
256 bubbles: [
257 {
258 text: "Here's how it looks. Happy with it, or want to try another?",
259 img: "$myAvatar",
260 imgAlt: "Your profile photo",
261 imgRound: true,
262 local: true,
263 },
264 ],
265 chips: [
266 {
267 label: "Keep this photo",
268 spent: "Kept",
269 record: { step: "photo", label: "set your profile photo" },
270 goto: "finish",
271 },
272 {
273 label: "Use a different photo",
274 gotoAsk: {
275 step: "photo",
276 text: "Of course! Send me another one whenever you're ready β€” and pop a message with it if you'd like a description. πŸ“·",
277 },
278 },
279 ],
280 },
281 {
282 id: "finish",
283 // The completion marker: hosts without the web's localStorage flag (the
284 // native embed) read this recorded step to know the conversation is
285 // over and must not re-arm.
286 record: { label: "You're all set", value: "done" },
287 effects: [{ verb: "markWelcomeDone" }],
288 bubbles: [
289 {
290 text: [
291 { if: "vars.skippedPhoto", text: "No problem β€” you can add one anytime from your profile.\n\n" },
292 "That's it β€” you're all set. πŸŽ‰ Enjoy {space.name|this space}!",
293 // Open groups: existence only, never names β€” a locked level's name
294 // stays sealed by design, and the directory is one tap away.
295 {
296 if: "openGroups",
297 text: "\n\nThis space also has open groups anyone here can join β€” have a look around whenever you like.",
298 },
299 // The one honest parting warning for someone who chose "Not now":
300 // plain consequences, no mechanism talk, and the way out right there.
301 {
302 if: { all: ["!accessBackedUp", "bridge.canEnrol"] },
303 text:
304 "\n\nOne more reminder before you go: you haven't signed in yet, so your access to this space lives only in this browser β€” if it's cleared or lost, there's no way back in. Sign in any time and you're safe.",
305 },
306 ],
307 key: "q-finish",
308 },
309 { text: "Where would you like to start?" },
310 ],
311 chips: [
312 // The parting reminder's action β€” finish-line chips stay live, and the
313 // completion poll picks the sign-in up even after the flow has ended.
314 { if: { all: ["!accessBackedUp", "bridge.canEnrol"] }, label: "Sign in", advancing: false, effect: { verb: "savePasskey" } },
315 {
316 if: { all: ["hasPendingThread", "inviterName"] },
317 label: "Say hi to {inviterName}",
318 advancing: false,
319 effect: { verb: "openPendingThread" },
320 },
321 {
322 if: { all: ["hasPendingThread", "!inviterName"] },
323 label: "Open your first chat",
324 advancing: false,
325 effect: { verb: "openPendingThread" },
326 },
327 { if: "openGroups", label: "Browse open groups", advancing: false, effect: { verb: "openDiscover" } },
328 // The tour is the web's guided overlay β€” web-only for now.
329 { if: "!native", label: "Take the tour", advancing: false, effect: { verb: "startTour" } },
330 // "Where would you like to start?" needs an answer that ISN'T a
331 // destination: most people are finished, and every chip pointing
332 // somewhere else made leaving feel like the one thing on offer.
333 // Advancing (so it spends itself), and it says the thing out loud β€”
334 // you're set up, nothing is outstanding, Eleven is here if you want it.
335 { label: "Done for now", spent: "Done", say: "You're all set. πŸ‘ I'm here whenever you want me β€” just say hello, or type `/` to see what I can do." },
336 ],
337 end: true,
338 },
339 ],
340};
341