eleven / messenger Public

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

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): access is already secured by Sign in
43 // with Eleven β€” no browser key to lose, so the passkey ask is moot.
44 if: "native",
45 recordIfNew: { label: "Your access is already secured by your Eleven sign-in", value: "native" },
46 goto: "name",
47 },
48 {
49 if: { any: ["bridge.enrolled", "done"] },
50 recordIfNew: { label: "Signed in β€” your access is safe", value: "passkey" },
51 // Just back from the sign-in ceremony: acknowledge it worked, then
52 // move on β€” a separate bubble, so the ack lands before the question.
53 // Covers BOTH arrivals: the mid-conversation return leg AND a joiner
54 // who signed up before the conversation started (signup-first join) β€”
55 // so no "welcome back", which reads wrong when nobody left.
56 sayIfNew: "Great β€” you're signed in, so your access is safe. πŸ”’ Let's continue.",
57 goto: "name",
58 },
59 // No home server to sign in with (self-hosted): the ask would be a
60 // dead-end chip β€” record the honest state and carry on (#1377).
61 {
62 if: "!bridge.canEnrol",
63 recordIfNew: { label: "Your access lives in this browser", value: "local" },
64 goto: "name",
65 },
66 ],
67 // The sign-in ceremony returns asynchronously (a redirect back, or tabbing
68 // back from a system browser); auto-detect that return and carry on without
69 // a manual "Continue" tap.
70 settle: { when: "bridge.enrolled" },
71 // One bubble, chips attached β€” context and ask together, so the opening
72 // run is two messages (intro + this), not three. The member-facing word
73 // is "sign in" β€” never the mechanism (passkeys, backups).
74 bubbles: [
75 {
76 text:
77 "Before anything else, let's make sure you can't lose your way in. πŸ”‘\n\n" +
78 "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" +
79 "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.",
80 },
81 ],
82 chips: [
83 {
84 label: "Sign in (recommended)",
85 advancing: false,
86 effect: { verb: "savePasskey" },
87 probe: { afterMs: 1500, unless: "bridge.enrolled", goto: "access-waiting" },
88 },
89 {
90 label: "Not now",
91 spent: "Skipped",
92 effect: { verb: "deferBackupReminders" },
93 record: { label: "Skipped signing in for now", value: "skipped" },
94 ack: "No problem β€” I'll nudge you again in a day or so.",
95 goto: "name",
96 },
97 // The fast path for people in a hurry: one form with every question at
98 // once, ending in the same sign-in. Lives on this step deliberately β€”
99 // its settle (bridge.enrolled) is what carries the flow on after the
100 // form's fast-filled records + sign-in hand-off.
101 {
102 label: "Fill in a form instead",
103 trailing: true,
104 advancing: false,
105 effect: { verb: "openQuickForm" },
106 },
107 ],
108 },
109 {
110 id: "access-waiting",
111 // The ceremony may confirm while we sit here (the completion poll flips
112 // the flag) β€” re-enter access so its entry records + acks, no tap needed.
113 settle: { when: "bridge.enrolled", goto: "access" },
114 bubbles: [{ text: "I'll wait here while you finish signing in. When you're done, tap Continue." }],
115 chips: [
116 {
117 label: "Continue",
118 record: {
119 step: "access",
120 label: {
121 if: "bridge.enrolled",
122 then: "Signed in β€” your access is safe",
123 else: "Signed in (in this browser)",
124 },
125 value: "passkey",
126 },
127 ack: "Perfect β€” you're all set. πŸ”’",
128 goto: "name",
129 },
130 {
131 label: "It didn't work β€” skip for now",
132 spent: "Skipped",
133 effect: { verb: "deferBackupReminders" },
134 record: { step: "access", label: "Skipped signing in for now", value: "skipped" },
135 ack: "No problem β€” I'll nudge you again in a day or so.",
136 goto: "name",
137 },
138 ],
139 },
140 {
141 id: "name",
142 skipIf: ["done", "bridge.myName"],
143 bubbles: [
144 {
145 text: "{ack?}First things first β€” what should folks here call you? Reply with your name.",
146 key: "q-name",
147 label: "Your name:",
148 hint: "e.g. Finn Wolfhard",
149 },
150 ],
151 chips: [
152 {
153 if: "suggested",
154 label: "I'm {suggested}",
155 set: { "vars.answer": "{suggested}" },
156 effect: { verb: "setMyName", args: ["$answer"] },
157 record: { label: "noted: you're {vars.answer}", value: "$answer" },
158 ack: "Lovely to meet you, {vars.answer}! 🀝",
159 goto: "photo",
160 },
161 ],
162 input: { kind: "text", required: true, maxLen: 32, clean: "name" },
163 effect: { verb: "setMyName", args: ["$answer"] },
164 on: {
165 ok: {
166 record: { label: "noted: you're {vars.answer}", value: "$answer" },
167 ack: "Lovely to meet you, {vars.answer}! 🀝",
168 goto: "photo",
169 },
170 },
171 },
172 {
173 id: "photo",
174 // Native v1: no attachment flow in the conversation composer β€” the
175 // profile photo lives in your profile there.
176 skipIf: ["done", "native"],
177 skipTo: "finish",
178 bubbles: [
179 {
180 text: "{ack?}Want to add a profile photo? Only members of this space can see it β€” like everything else here, it's encrypted.",
181 key: "q-photo",
182 },
183 {
184 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.",
185 attachHint: "Optional: describe your photo",
186 },
187 ],
188 chips: [
189 {
190 label: "Skip for now",
191 spent: "Skipped",
192 cancelInput: true,
193 set: { "vars.skippedPhoto": true },
194 // Recorded so a resume (notably the passkey return leg) doesn't
195 // re-ask a step the person already waved away. A skip is an answer.
196 record: { label: "skipped the profile photo", value: "skipped" },
197 goto: "finish",
198 },
199 ],
200 input: {
201 kind: "photo",
202 reject: { text: "Send it as a picture β€” tap the + next to the message box or drag one in β€” or tap Skip.", local: true },
203 },
204 effect: { verb: "adoptProfilePhoto" },
205 on: {
206 ok: { goto: "photo-preview" },
207 nokey: {
208 say: "One sec β€” I'm still finishing your secure setup. Try sending it again in a moment, or skip for now.",
209 local: true,
210 ask: "Ready when you are β€” send that photo again, or skip it.",
211 },
212 error: {
213 say: "Hmm, I couldn't use that one β€” try another?",
214 local: true,
215 ask: "Of course! Send me another one whenever you're ready β€” and pop a message with it if you'd like a description. πŸ“·",
216 },
217 },
218 },
219 {
220 id: "photo-preview",
221 // The preview is a client-only notice: the photo is an encrypted blob,
222 // so its decrypted object URL can't ride a persisted event.
223 bubbles: [
224 {
225 text: "Here's how it looks. Happy with it, or want to try another?",
226 img: "$myAvatar",
227 imgAlt: "Your profile photo",
228 imgRound: true,
229 local: true,
230 },
231 ],
232 chips: [
233 {
234 label: "Keep this photo",
235 spent: "Kept",
236 record: { step: "photo", label: "set your profile photo" },
237 goto: "finish",
238 },
239 {
240 label: "Use a different photo",
241 gotoAsk: {
242 step: "photo",
243 text: "Of course! Send me another one whenever you're ready β€” and pop a message with it if you'd like a description. πŸ“·",
244 },
245 },
246 ],
247 },
248 {
249 id: "finish",
250 // The completion marker: hosts without the web's localStorage flag (the
251 // native embed) read this recorded step to know the conversation is
252 // over and must not re-arm.
253 record: { label: "You're all set", value: "done" },
254 effects: [{ verb: "markWelcomeDone" }],
255 bubbles: [
256 {
257 text: [
258 { if: "vars.skippedPhoto", text: "No problem β€” you can add one anytime from your profile.\n\n" },
259 "That's it β€” you're all set. πŸŽ‰ Enjoy {space.name|this space}!",
260 // Open groups: existence only, never names β€” a locked level's name
261 // stays sealed by design, and the directory is one tap away.
262 {
263 if: "openGroups",
264 text: "\n\nThis space also has open groups anyone here can join β€” have a look around whenever you like.",
265 },
266 // The one honest parting warning for someone who chose "Not now":
267 // plain consequences, no mechanism talk, and the way out right there.
268 {
269 if: { all: ["!accessBackedUp", "bridge.canEnrol"] },
270 text:
271 "\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.",
272 },
273 ],
274 key: "q-finish",
275 },
276 { text: "Where would you like to start?" },
277 ],
278 chips: [
279 // The parting reminder's action β€” finish-line chips stay live, and the
280 // completion poll picks the sign-in up even after the flow has ended.
281 { if: { all: ["!accessBackedUp", "bridge.canEnrol"] }, label: "Sign in", advancing: false, effect: { verb: "savePasskey" } },
282 {
283 if: { all: ["hasPendingThread", "inviterName"] },
284 label: "Say hi to {inviterName}",
285 advancing: false,
286 effect: { verb: "openPendingThread" },
287 },
288 {
289 if: { all: ["hasPendingThread", "!inviterName"] },
290 label: "Open your first chat",
291 advancing: false,
292 effect: { verb: "openPendingThread" },
293 },
294 { if: "openGroups", label: "Browse open groups", advancing: false, effect: { verb: "openDiscover" } },
295 // The tour is the web's guided overlay β€” web-only for now.
296 { if: "!native", label: "Take the tour", advancing: false, effect: { verb: "startTour" } },
297 ],
298 end: true,
299 },
300 ],
301};
302