eleven / messenger Public

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

Plain git — no account needed to clone.

Download

Download this file

1{
2 "schema": 1,
3 "flow": "setup",
4 "entry": "hello",
5 "resumedIf": {
6 "any": [
7 "done.name",
8 "done.group",
9 "done.email",
10 "done.photo",
11 "done.invites",
12 "done.extras",
13 "done.keys"
14 ]
15 },
16 "resumeUnless": {
17 "all": [
18 "bridge.enrolled",
19 "!done.keys"
20 ]
21 },
22 "resumeSay": "Welcome back β€” let's pick up where we left off.",
23 "steps": [
24 {
25 "id": "hello",
26 "skipIf": [
27 "resumed"
28 ],
29 "bubbles": [
30 {
31 "text": "Hi β€” I'm Eleven! πŸ‘‹ This is your space now: everything in it is end-to-end encrypted, and you're its administrator.\n\nLet's take two minutes to set things up."
32 }
33 ],
34 "auto": "name"
35 },
36 {
37 "id": "name",
38 "entries": [
39 {
40 "if": "bridge.myName",
41 "recordIfNew": {
42 "label": "noted the name you go by",
43 "value": "known"
44 },
45 "goto": "group"
46 }
47 ],
48 "skipIf": [
49 "done"
50 ],
51 "bubbles": [
52 {
53 "text": "First β€” what should everyone here call you? Reply with your name.",
54 "label": "Your name:",
55 "hint": "e.g. Finn Wolfhard"
56 }
57 ],
58 "chips": [
59 {
60 "label": "Fill in a form instead",
61 "trailing": true,
62 "advancing": false,
63 "effect": {
64 "verb": "openQuickForm"
65 }
66 }
67 ],
68 "input": {
69 "kind": "text",
70 "required": true,
71 "maxLen": 32,
72 "clean": "name"
73 },
74 "effect": {
75 "verb": "setMyName",
76 "args": [
77 "$answer"
78 ]
79 },
80 "on": {
81 "ok": {
82 "record": {
83 "label": "noted: you're {vars.answer}",
84 "value": "$answer"
85 },
86 "ack": "Lovely to meet you, {vars.answer}! 🀝",
87 "goto": "group"
88 }
89 }
90 },
91 {
92 "id": "group",
93 "skipIf": [
94 "done"
95 ],
96 "bubbles": [
97 {
98 "text": "{ack?}Next, this space needs a name. It's the one everyone sees β€” on the sign-in page, in invites, on the app icon.",
99 "key": "q-group"
100 },
101 {
102 "text": {
103 "if": "space.nameSet",
104 "then": "It's currently β€œ{space.name}” β€” keep that, or reply with a new name.",
105 "else": "What should we call it? Reply with a name."
106 },
107 "label": "Space name:",
108 "hint": "e.g. The Book Club"
109 }
110 ],
111 "chips": [
112 {
113 "if": "space.nameSet",
114 "label": "Keep β€œ{space.name}”",
115 "spent": "Kept",
116 "goto": "email"
117 }
118 ],
119 "input": {
120 "kind": "text",
121 "required": true
122 },
123 "effect": {
124 "verb": "setGroupName",
125 "args": [
126 "$answer"
127 ]
128 },
129 "on": {
130 "ok": {
131 "set": {
132 "space.name": "$answer"
133 },
134 "record": {
135 "label": "named this space β€œ{vars.answer}”",
136 "value": "$answer"
137 },
138 "goto": "email"
139 },
140 "invalid": {
141 "say": "Hmm, that name didn't stick β€” it needs to be 1–60 characters. Try another?",
142 "retry": "step"
143 },
144 "error": {
145 "say": "Something went wrong saving that β€” it's not the name's fault. Give it another go in a moment.",
146 "retry": "step"
147 }
148 }
149 },
150 {
151 "id": "email",
152 "skipIf": [
153 "done"
154 ],
155 "bubbles": [
156 {
157 "text": "Your contact email is shared with members β€” useful if someone's locked out, or something needs a human. It's optional."
158 },
159 {
160 "text": "How can people here reach you outside the chat? Reply with an email address, or skip it.",
161 "label": "Contact email:",
162 "hint": "e.g. finn@example.com"
163 }
164 ],
165 "chips": [
166 {
167 "label": "Skip for now",
168 "spent": "Skipped",
169 "record": {
170 "label": "skipped the contact email",
171 "value": "skipped"
172 },
173 "goto": "photo"
174 }
175 ],
176 "input": {
177 "kind": "text"
178 },
179 "effect": {
180 "verb": "setContactEmail",
181 "args": [
182 "$answer"
183 ]
184 },
185 "on": {
186 "ok": {
187 "record": {
188 "label": "noted a contact email",
189 "value": "$answer"
190 },
191 "goto": "photo"
192 },
193 "invalid": {
194 "ask": "That doesn't look like an email address β€” try again, or skip it."
195 },
196 "error": {
197 "ask": "Something went wrong saving that β€” try again in a moment, or skip it."
198 }
199 }
200 },
201 {
202 "id": "photo",
203 "skipIf": [
204 "done",
205 "native"
206 ],
207 "skipTo": "invites",
208 "bubbles": [
209 {
210 "text": "Want to give this space a face? The photo shows on the welcome page and when people join β€” you can always change it later."
211 },
212 {
213 "text": "Send me a photo β€” tap the + next to the message box, or just drag a picture into the chat, and hit send. Type a message with it if you want to include alt text.",
214 "attachHint": "Optional: type alt text for the photo"
215 }
216 ],
217 "chips": [
218 {
219 "label": "Skip for now",
220 "spent": "Skipped",
221 "cancelInput": true,
222 "record": {
223 "label": "skipped the space photo",
224 "value": "skipped"
225 },
226 "goto": "invites"
227 }
228 ],
229 "input": {
230 "kind": "photo",
231 "reject": {
232 "text": "Send it as a picture β€” tap the + next to the message box or drag one in β€” or tap Skip."
233 }
234 },
235 "effect": {
236 "verb": "adoptServerPhoto"
237 },
238 "on": {
239 "ok": {
240 "set": {
241 "vars.photoAlt": "$answerText"
242 },
243 "goto": "photo-preview"
244 },
245 "error": {
246 "say": "Hmm, I couldn't use that one β€” try another?",
247 "ask": "Send another photo when you're ready, or skip it."
248 }
249 }
250 },
251 {
252 "id": "photo-preview",
253 "bubbles": [
254 {
255 "text": [
256 "Here's how it looks.",
257 {
258 "if": "vars.photoAlt",
259 "text": "\n\nIts alt text is the message you sent β€” β€œ{vars.photoAlt}” β€” and like the photo itself it's PUBLIC: anyone with an invite link can see both."
260 },
261 "\n\nHappy with it, or want to try another?"
262 ],
263 "img": "$serverAvatar",
264 "imgAlt": "{vars.photoAlt|The space photo}",
265 "local": true
266 }
267 ],
268 "chips": [
269 {
270 "label": "Keep this photo",
271 "spent": "Kept",
272 "record": {
273 "step": "photo",
274 "label": "set the space photo",
275 "value": "{vars.photoAlt}"
276 },
277 "goto": "invites"
278 },
279 {
280 "label": "Use a different photo",
281 "gotoAsk": {
282 "step": "photo",
283 "text": "Of course! Send me another one whenever you're ready β€” and pop a message with it if you'd like alt text. πŸ“·"
284 }
285 }
286 ]
287 },
288 {
289 "id": "invites",
290 "skipIf": [
291 "done"
292 ],
293 "bubbles": [
294 {
295 "text": "Who gets to bring people in? You can always invite people. The question is whether members can too, or whether new faces only arrive through you."
296 }
297 ],
298 "chips": [
299 {
300 "label": "Members can invite people",
301 "effect": {
302 "verb": "setUserInvites",
303 "args": [
304 true
305 ]
306 },
307 "record": {
308 "label": "decided members can invite people",
309 "value": "members"
310 },
311 "goto": "extras"
312 },
313 {
314 "label": "Only me",
315 "effect": {
316 "verb": "setUserInvites",
317 "args": [
318 false
319 ]
320 },
321 "record": {
322 "label": "decided only the admin invites people",
323 "value": "admin only"
324 },
325 "goto": "extras"
326 }
327 ]
328 },
329 {
330 "id": "extras",
331 "skipIf": [
332 "done"
333 ],
334 "skipTo": "ai-optin",
335 "entries": [
336 {
337 "if": "done.extras-apps",
338 "recordIfNew": {
339 "label": "chose this space's extras",
340 "value": "chosen"
341 },
342 "goto": "ai-optin"
343 }
344 ],
345 "bubbles": [
346 {
347 "text": "Nearly there. Your space comes with a couple of extras:\n\nβ€’ Bots β€” little helpers members can create for reminders and jobs.\nβ€’ Apps β€” to-dos, polls, whiteboards, and a studio for building your own.\n\nEverything essential works with these off, and you can change your mind anytime in Admin."
348 }
349 ],
350 "chips": [
351 {
352 "label": "Keep both on",
353 "spent": "Kept",
354 "record": {
355 "label": "kept bots and apps on",
356 "value": "all-on"
357 },
358 "goto": "ai-optin"
359 },
360 {
361 "label": "Let me choose",
362 "goto": "extras-bots"
363 }
364 ]
365 },
366 {
367 "id": "extras-bots",
368 "skipIf": [
369 "done"
370 ],
371 "bubbles": [
372 {
373 "text": "Bots β€” helpers that live in chats and do jobs: reminders, summaries, little tools. Members can create their own."
374 }
375 ],
376 "chips": [
377 {
378 "label": "Enable bots",
379 "effect": {
380 "verb": "setFeature",
381 "args": [
382 "bots",
383 true
384 ]
385 },
386 "record": {
387 "label": "enabled bots",
388 "value": "on"
389 },
390 "goto": "extras-apps"
391 },
392 {
393 "label": "No bots here",
394 "spent": "Off",
395 "effect": {
396 "verb": "setFeature",
397 "args": [
398 "bots",
399 false
400 ]
401 },
402 "record": {
403 "label": "switched bots off",
404 "value": "off"
405 },
406 "goto": "extras-apps"
407 }
408 ]
409 },
410 {
411 "id": "extras-apps",
412 "skipIf": [
413 "done"
414 ],
415 "bubbles": [
416 {
417 "text": "And apps β€” the built-in extras like to-dos, polls, and whiteboards, plus the App Studio where members build their own."
418 }
419 ],
420 "chips": [
421 {
422 "label": "Enable apps",
423 "effect": {
424 "verb": "setFeature",
425 "args": [
426 "apps",
427 true
428 ]
429 },
430 "record": {
431 "label": "enabled apps",
432 "value": "on"
433 },
434 "goto": "ai-optin"
435 },
436 {
437 "label": "No apps here",
438 "spent": "Off",
439 "effect": {
440 "verb": "setFeature",
441 "args": [
442 "apps",
443 false
444 ]
445 },
446 "record": {
447 "label": "switched apps off",
448 "value": "off"
449 },
450 "goto": "ai-optin"
451 }
452 ]
453 },
454 {
455 "id": "ai-optin",
456 "skipIf": [
457 "done"
458 ],
459 "bubbles": [
460 {
461 "text": "Last choice β€” AI. Say yes and Eleven, the built-in helper, joins this space: members can ask it things in plain language and it answers, builds, and helps out.\n\nAn honest note before you decide: Eleven's AI runs on Eleven Messenger's machines, and it can read the chats it's invited to β€” that's what lets it help. It never sees chats it isn't in, and you can switch it off anytime in Admin."
462 }
463 ],
464 "chips": [
465 {
466 "label": "Bring Eleven in",
467 "spent": "On",
468 "effect": {
469 "verb": "setFeature",
470 "args": [
471 "ai",
472 true
473 ]
474 },
475 "record": {
476 "label": "invited Eleven's AI in",
477 "value": "on"
478 },
479 "goto": "plan"
480 },
481 {
482 "label": "Not now",
483 "spent": "Off",
484 "effect": {
485 "verb": "setFeature",
486 "args": [
487 "ai",
488 false
489 ]
490 },
491 "record": {
492 "label": "kept AI off",
493 "value": "off"
494 },
495 "goto": "plan"
496 }
497 ]
498 },
499 {
500 "id": "plan",
501 "skipIf": [
502 "done",
503 "native"
504 ],
505 "skipTo": "keys",
506 "entries": [
507 {
508 "if": "!bridge.planPrices",
509 "goto": "keys"
510 }
511 ],
512 "bubbles": [
513 {
514 "text": "One more question β€” who is this space for?\n\nEvery space starts free, and free is a real home β€” kept that way by the spaces that pay. If this one's for a team, a community, or family, there's a plan that backs it (and the free spaces too).",
515 "key": "q-plan"
516 }
517 ],
518 "chips": [
519 {
520 "if": "bridge.planTeam",
521 "label": "A team",
522 "goto": "plan-team"
523 },
524 {
525 "if": "bridge.planCommunity",
526 "label": "A community",
527 "goto": "plan-community"
528 },
529 {
530 "if": "bridge.planFamily",
531 "label": "Family & friends",
532 "goto": "plan-family"
533 },
534 {
535 "label": "Keep it free",
536 "spent": "Free",
537 "trailing": true,
538 "record": {
539 "label": "kept this space free",
540 "value": "free"
541 },
542 "goto": "keys"
543 }
544 ]
545 },
546 {
547 "id": "plan-team",
548 "bubbles": [
549 {
550 "text": "For a team that's {bridge.planNameTeam}: {bridge.planPriceTeam} β€” it scales with the team, and bots are free.\n\nA paid space never counts against your space limit, is never reclaimed as unused, and carries a bigger storage allowance.",
551 "key": "q-plan-team"
552 }
553 ],
554 "chips": [
555 {
556 "label": "Choose {bridge.planNameTeam}",
557 "spent": "Chosen",
558 "set": {
559 "vars.planTier": "team"
560 },
561 "record": {
562 "step": "plan",
563 "label": "chose the {bridge.planNameTeam} plan β€” payment waits at the finish line",
564 "value": "team"
565 },
566 "goto": "keys"
567 },
568 {
569 "label": "Keep it free",
570 "spent": "Free",
571 "record": {
572 "step": "plan",
573 "label": "kept this space free",
574 "value": "free"
575 },
576 "goto": "keys"
577 }
578 ]
579 },
580 {
581 "id": "plan-community",
582 "bubbles": [
583 {
584 "text": "For a community that's {bridge.planNameCommunity}: {bridge.planPriceCommunity}.\n\nA paid space never counts against your space limit, is never reclaimed as unused, and carries a bigger storage allowance.",
585 "key": "q-plan-community"
586 }
587 ],
588 "chips": [
589 {
590 "label": "Choose {bridge.planNameCommunity}",
591 "spent": "Chosen",
592 "set": {
593 "vars.planTier": "community"
594 },
595 "record": {
596 "step": "plan",
597 "label": "chose the {bridge.planNameCommunity} plan β€” payment waits at the finish line",
598 "value": "community"
599 },
600 "goto": "keys"
601 },
602 {
603 "label": "Keep it free",
604 "spent": "Free",
605 "record": {
606 "step": "plan",
607 "label": "kept this space free",
608 "value": "free"
609 },
610 "goto": "keys"
611 }
612 ]
613 },
614 {
615 "id": "plan-family",
616 "bubbles": [
617 {
618 "text": "For family and friends that's {bridge.planNameFamily}: {bridge.planPriceFamily}.\n\nA paid space never counts against your space limit, is never reclaimed as unused, and carries a bigger storage allowance.",
619 "key": "q-plan-family"
620 }
621 ],
622 "chips": [
623 {
624 "label": "Choose {bridge.planNameFamily}",
625 "spent": "Chosen",
626 "set": {
627 "vars.planTier": "family"
628 },
629 "record": {
630 "step": "plan",
631 "label": "chose the {bridge.planNameFamily} plan β€” payment waits at the finish line",
632 "value": "family"
633 },
634 "goto": "keys"
635 },
636 {
637 "label": "Keep it free",
638 "spent": "Free",
639 "record": {
640 "step": "plan",
641 "label": "kept this space free",
642 "value": "free"
643 },
644 "goto": "keys"
645 }
646 ]
647 },
648 {
649 "id": "keys",
650 "entries": [
651 {
652 "if": "native",
653 "set": {
654 "vars.backedUp": true
655 },
656 "recordIfNew": {
657 "label": "Your space's key is saved with your Eleven account",
658 "value": "native"
659 },
660 "goto": "finish"
661 },
662 {
663 "if": "bridge.enrolled",
664 "set": {
665 "vars.backedUp": true
666 },
667 "recordIfNew": {
668 "label": "Signed in β€” your space's key is safe",
669 "value": "passkey"
670 },
671 "sayIfNew": "You're signed in β€” your space's key is safe. βœ…",
672 "goto": "finish"
673 },
674 {
675 "if": "done",
676 "goto": "finish"
677 },
678 {
679 "if": "!bridge.canEnrol",
680 "recordIfNew": {
681 "label": "This browser holds this space's key",
682 "value": "local"
683 },
684 "goto": "finish"
685 },
686 {
687 "if": "bridge.fromHome",
688 "goto": "keys-home"
689 }
690 ],
691 "settle": {
692 "when": "bridge.enrolled"
693 },
694 "bubbles": [
695 {
696 "text": "Now the most important thing of all. πŸ”‘\n\nEverything here is locked with a key that exists only on your devices. There's no password reset and no support desk β€” not even whoever runs the server can read your messages or recover that key. That's not a flaw; it's the whole point.\n\nBut it means: right now, this browser holds the only key to your space. If it's lost β€” cleared, broken, stolen β€” your space is gone with it, and nobody can get it back."
697 },
698 {
699 "text": "Sign in with Eleven and your key stays safe β€” one tap, your face or fingerprint, and you'll be brought straight back."
700 }
701 ],
702 "chips": [
703 {
704 "label": "Sign in",
705 "advancing": false,
706 "effect": {
707 "verb": "savePasskey"
708 },
709 "probe": {
710 "afterMs": 1500,
711 "unless": "bridge.enrolled",
712 "goto": "keys-waiting"
713 }
714 },
715 {
716 "label": "Not now",
717 "spent": "Skipped",
718 "goto": "keys-confirm"
719 }
720 ]
721 },
722 {
723 "id": "keys-home",
724 "entries": [
725 {
726 "if": "bridge.enrolled",
727 "set": {
728 "vars.backedUp": true
729 },
730 "recordIfNew": {
731 "label": "Signed in β€” your space's key is safe",
732 "value": "passkey"
733 },
734 "sayIfNew": "You're signed in β€” your space's key is safe. βœ…",
735 "goto": "finish"
736 }
737 ],
738 "settle": {
739 "when": "bridge.enrolled"
740 },
741 "bubbles": [
742 {
743 "text": "Last thing β€” the key. πŸ”‘\n\nThis space is locked with a key that was just created here, in this browser. Your Eleven sign-in doesn't hold it yet β€” so if this browser is lost, the space goes with it."
744 },
745 {
746 "text": "Sign in and it's added to your account β€” one tap, and you'll be brought straight back."
747 }
748 ],
749 "chips": [
750 {
751 "label": "Sign in",
752 "advancing": false,
753 "effect": {
754 "verb": "savePasskey"
755 },
756 "probe": {
757 "afterMs": 1500,
758 "unless": "bridge.enrolled",
759 "goto": "keys-waiting"
760 }
761 },
762 {
763 "label": "Not now",
764 "spent": "Skipped",
765 "goto": "keys-confirm"
766 }
767 ]
768 },
769 {
770 "id": "keys-waiting",
771 "settle": {
772 "when": "bridge.enrolled",
773 "goto": "keys"
774 },
775 "bubbles": [
776 {
777 "text": "I'll wait here while you finish signing in. When you're done, tap Continue."
778 }
779 ],
780 "chips": [
781 {
782 "label": "Continue",
783 "record": {
784 "step": "keys",
785 "label": {
786 "if": "bridge.enrolled",
787 "then": "Signed in β€” your space's key is safe",
788 "else": "Signed in (in this browser)"
789 },
790 "value": "passkey"
791 },
792 "set": {
793 "vars.backedUp": true
794 },
795 "goto": "finish"
796 },
797 {
798 "label": "It didn't work β€” skip for now",
799 "spent": "Skipped",
800 "record": {
801 "step": "keys",
802 "label": "Skipped signing in for now",
803 "value": "skipped"
804 },
805 "set": {
806 "vars.backedUp": false
807 },
808 "goto": "finish"
809 }
810 ]
811 },
812 {
813 "id": "keys-confirm",
814 "settle": {
815 "when": "bridge.enrolled",
816 "goto": "keys"
817 },
818 "bubbles": [
819 {
820 "text": "I have to be straight with you: if you don't sign in, one lost or cleared browser means this space and everything in it is gone β€” permanently, for everyone, and no one can help. I'll remind you again in a day or so."
821 }
822 ],
823 "chips": [
824 {
825 "label": "Go back and sign in",
826 "advancing": false,
827 "effect": {
828 "verb": "savePasskey"
829 },
830 "probe": {
831 "afterMs": 1500,
832 "unless": "bridge.enrolled",
833 "goto": "keys-waiting"
834 }
835 },
836 {
837 "label": "I understand the risk",
838 "spent": "Understood",
839 "record": {
840 "step": "keys",
841 "label": "Skipped signing in for now",
842 "value": "skipped"
843 },
844 "set": {
845 "vars.backedUp": false
846 },
847 "goto": "finish"
848 }
849 ]
850 },
851 {
852 "id": "finish",
853 "record": {
854 "label": "Your space is ready",
855 "value": "done"
856 },
857 "effects": [
858 {
859 "if": "!vars.backedUp",
860 "verb": "deferBackupReminders"
861 },
862 {
863 "verb": "markSetupComplete"
864 }
865 ],
866 "bubbles": [
867 {
868 "text": [
869 "That's it β€” {space.name|your space} is ready! πŸŽ‰\n\n",
870 "Everything we just set up lives in your Admin panel β€” invite links, the space's name and photo, who can invite. Type `/admin` anywhere to open it; it's yours alone as this space's admin.",
871 {
872 "if": {
873 "all": [
874 "!vars.backedUp",
875 "bridge.canEnrol"
876 ]
877 },
878 "text": "\n\nOne more reminder before you go: you haven't signed in, so this browser holds the only key to this space. If it's lost or cleared, the space is gone with it β€” for everyone. Sign in any time and it's safe."
879 },
880 {
881 "if": "vars.planTier",
882 "text": "\n\nAnd the plan you picked β€” tap below whenever you're ready to set up the payment. Your space runs free until then."
883 }
884 ],
885 "key": "q-finish"
886 },
887 {
888 "text": "Where would you like to start?"
889 }
890 ],
891 "chips": [
892 {
893 "if": {
894 "all": [
895 "!vars.backedUp",
896 "bridge.canEnrol"
897 ]
898 },
899 "label": "Sign in",
900 "advancing": false,
901 "effect": {
902 "verb": "savePasskey"
903 }
904 },
905 {
906 "if": "vars.planTier",
907 "label": "Set up your plan",
908 "advancing": false,
909 "effect": {
910 "verb": "openPlanCheckout"
911 }
912 },
913 {
914 "label": "Open admin",
915 "advancing": false,
916 "effect": {
917 "verb": "openAdmin"
918 }
919 },
920 {
921 "if": "!native",
922 "label": "Take the tour",
923 "advancing": false,
924 "effect": {
925 "verb": "startTour"
926 }
927 }
928 ],
929 "end": true
930 }
931 ]
932}
933