| 1 | <meta charset="utf-8" /> |
| 2 | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> |
| 3 | <meta name="theme-color" content="#5b6cff" /> |
| 4 | <title>__GROUP_NAME__</title> |
| 5 | |
| 6 | <!-- Link-preview card (iMessage/Slack/WhatsApp/…). Crawlers fetch this URL with no |
| 7 | JS and no #fragment, so the card can only show what the server knows in the |
| 8 | clear: the group name (plaintext branding) and the server avatar. The inviter's |
| 9 | screen name is E2E — never sent to the server — so it deliberately can't appear |
| 10 | here. __OG_IMAGE__ is the server avatar when set, else the app logo. --> |
| 11 | <meta property="og:type" content="website" /> |
| 12 | <meta property="og:site_name" content="__GROUP_NAME__" /> |
| 13 | <meta property="og:url" content="__OG_URL__" /> |
| 14 | <meta property="og:title" content="You're invited to __GROUP_NAME__" /> |
| 15 | <meta property="og:description" content="Private, end-to-end encrypted chat." /> |
| 16 | <meta property="og:image" content="__OG_IMAGE__" /> |
| 17 | <meta name="twitter:card" content="summary" /> |
| 18 | <meta name="twitter:title" content="You're invited to __GROUP_NAME__" /> |
| 19 | <meta name="twitter:description" content="Private, end-to-end encrypted chat." /> |
| 20 | <meta name="twitter:image" content="__OG_IMAGE__" /> |
| 21 | <!-- Native wrapper detection: the Mac app's user agent carries "lchatMac". Tag the |
| 22 | document so CSS can adopt native conventions (e.g. drop web hand cursors). --> |
| 23 | <script nonce="__CSP_NONCE__"> |
| 24 | if (navigator.userAgent.includes("lchatMac")) |
| 25 | document.documentElement.classList.add("native"); |
| 26 | </script> |
| 27 | <!-- The home server (sign-in / passkey backup; server-configured via |
| 28 | LCHAT_HOME_ORIGIN — same substitution as index.html). The error card's |
| 29 | "Sign in" door reads it; the origin is public, so exposing it is fine. --> |
| 30 | <script nonce="__CSP_NONCE__"> |
| 31 | window.__homeOrigin = "__HOME_ORIGIN__"; |
| 32 | </script> |
| 33 | <link rel="stylesheet" href="/static/style.css" /> |
| 34 | <!-- Same identity as the chat root, so "Add to Home Screen" from here installs as |
| 35 | __GROUP_NAME__ (not a generic "Join") and launches at the app's start_url ("/"). --> |
| 36 | <link rel="manifest" href="/manifest.webmanifest" /> |
| 37 | <link rel="icon" href="/static/icon-192.png" /> |
| 38 | <link rel="apple-touch-icon" href="/static/icon-180.png" /> |
| 39 | <meta name="apple-mobile-web-app-capable" content="yes" /> |
| 40 | <meta name="apple-mobile-web-app-title" content="__GROUP_NAME__" /> |
| 41 | |
| 42 | <!-- Apply the saved theme before first paint (see web/static/ui.js — the canonical |
| 43 | list; this copy must stay in sync). A local preference, never sent to the server. --> |
| 44 | <script nonce="__CSP_NONCE__"> |
| 45 | (function () { |
| 46 | try { |
| 47 | var THEMES = ["litroom", "lagoon", "sunny", "plum", "linen", "classic"]; |
| 48 | var PANEL = { |
| 49 | litroom: ["#fff8f0", "#241a1e"], lagoon: ["#f3fbfa", "#0c2a29"], |
| 50 | sunny: ["#fffaf0", "#241d12"], plum: ["#fdf8fd", "#221829"], |
| 51 | linen: ["#f5f1ea", "#211f1b"], classic: ["#ffffff", "#1d2026"], |
| 52 | }; |
| 53 | var t = localStorage.getItem("lchat_theme"); |
| 54 | if (THEMES.indexOf(t) < 0) t = "classic"; |
| 55 | var a = localStorage.getItem("lchat_appearance"); |
| 56 | var root = document.documentElement; |
| 57 | root.dataset.theme = t; |
| 58 | root.style.colorScheme = a === "light" ? "light" : a === "dark" ? "dark" : "light dark"; |
| 59 | var dark = a === "dark" || (a !== "light" && matchMedia("(prefers-color-scheme: dark)").matches); |
| 60 | var m = document.querySelector('meta[name="theme-color"]'); |
| 61 | if (m) m.setAttribute("content", PANEL[t][dark ? 1 : 0]); |
| 62 | } catch (e) {} |
| 63 | })(); |
| 64 | </script> |
| 65 | |
| 66 | <!-- The one loading state: the same full-screen loader the app itself uses |
| 67 | (index.html #app-loading). It covers the page from load until the welcome |
| 68 | card is ready, and again while a claim is in flight — no per-card |
| 69 | "Loading…" / spinner-card variants. --> |
| 70 | <div id="app-loading" class="app-loading"><div class="spinner" aria-label="Loading"></div></div> |
| 71 | |
| 72 | <div class="center"> |
| 73 | <div class="card hidden" id="card"></div> |
| 74 | </div> |
| 75 | |
| 76 | <template id="homescreen-tpl"> |
| 77 | <div class="onboard-icon">📲</div> |
| 78 | <h1>You're in! Add it to your Home Screen</h1> |
| 79 | <p> |
| 80 | You're signed in here in Safari. On iPhone & iPad, add the chat to your Home |
| 81 | Screen too, so it opens like an app and can send you notifications. You'll sign in |
| 82 | there once, with the link below. |
| 83 | </p> |
| 84 | <ol class="steps"> |
| 85 | <li>Tap <strong>Copy sign-in link</strong> below.</li> |
| 86 | <li>Tap the <strong>Share</strong> button (the square with an up arrow) in Safari, then choose <strong>Add to Home Screen</strong>.</li> |
| 87 | <li>Open the app from your Home Screen and <strong>paste the link</strong> to sign in.</li> |
| 88 | </ol> |
| 89 | <div class="row"><button id="hs-copy">Copy sign-in link</button></div> |
| 90 | <div class="row"><button id="hs-open" class="secondary">Just use Safari for now</button></div> |
| 91 | </template> |
| 92 | |
| 93 | <template id="form-tpl"> |
| 94 | <img id="form-photo" class="onboard-photo hidden" alt="" /> |
| 95 | <h1>Pick a screen name</h1> |
| 96 | <p>This is what everyone in the chat will see. You can use your first name or a nickname.</p> |
| 97 | <form id="join-form" autocomplete="off"> |
| 98 | <input id="name" placeholder="e.g. Sophie" maxlength="32" autocomplete="off" /> |
| 99 | <div class="row"> |
| 100 | <button type="submit">Join the chat</button> |
| 101 | </div> |
| 102 | </form> |
| 103 | </template> |
| 104 | |
| 105 | <template id="error-tpl"> |
| 106 | <h1>This link doesn't work</h1> |
| 107 | <p id="error-msg">This invite link is invalid or has been turned off. Ask for a new one.</p> |
| 108 | <!-- Revealed by showError({signin}) for dead-invite errors on the web: a |
| 109 | returning member whose session is gone doesn't need a new invite — their |
| 110 | account gets them back in via the home sign-in page. Stays hidden in the |
| 111 | native add-chat webview (it owns navigation and expects a join) and on |
| 112 | self-hosts with no home server (no sign-in to offer). --> |
| 113 | <div class="row"><button id="error-signin" class="secondary hidden">Already a member? Sign in</button></div> |
| 114 | </template> |
| 115 | |
| 116 | <!-- The signup-first lead card (docs/superpowers/specs/2026-07-31-signup-first- |
| 117 | join-design.md): fresh joiners see the sign-in door BEFORE the app/web |
| 118 | choice, so their access is saved to their account from the start. ONE path |
| 119 | — sign in — not a choice: the skip is deliberately small print (a quiet |
| 120 | link revealing the classic welcome card), never a competing button. --> |
| 121 | <template id="prejoin-tpl"> |
| 122 | <img id="prejoin-photo" class="onboard-photo hidden" alt="" /> |
| 123 | <div class="onboard-icon" id="prejoin-icon">💬</div> |
| 124 | <h1 id="prejoin-title">You're invited to __GROUP_NAME__</h1> |
| 125 | <p id="prejoin-sub">Private, end-to-end encrypted chat.</p> |
| 126 | <p id="prejoin-desc" class="welcome-desc hidden"></p> |
| 127 | <div class="row"><button id="prejoin-signin">Sign in with Eleven</button></div> |
| 128 | <p class="welcome-caveat"> |
| 129 | Signing in first keeps your access safe — get back into this space from any |
| 130 | device, with nothing to remember. |
| 131 | </p> |
| 132 | <div class="row"><button id="prejoin-skip" class="link-quiet">Continue without signing in</button></div> |
| 133 | </template> |
| 134 | |
| 135 | <!-- The welcome card (deferred claim — see docs/onboarding-and-home-server.md). |
| 136 | Nothing is claimed until a button is pressed; the one-time secret stays in |
| 137 | the URL fragment. data-store-url is per-instance config (LCHAT_APP_STORE_URL), |
| 138 | empty when no app link is configured. --> |
| 139 | <template id="welcome-tpl" data-store-url="__APP_STORE_URL__"> |
| 140 | <img id="welcome-photo" class="onboard-photo hidden" alt="" /> |
| 141 | <div class="onboard-icon" id="welcome-icon">💬</div> |
| 142 | <h1 id="welcome-title">You're invited to __GROUP_NAME__</h1> |
| 143 | <p id="welcome-sub">Private, end-to-end encrypted chat.</p> |
| 144 | <!-- The space's admin-set description, when one is set: served on the invite |
| 145 | lookup (valid link-holders only, never the public read — see getKeyInvite), |
| 146 | so it can render here before the joiner holds any key. --> |
| 147 | <p id="welcome-desc" class="welcome-desc hidden"></p> |
| 148 | <div class="row"><button id="welcome-app">Open in the app</button></div> |
| 149 | <div class="row"><button id="welcome-store" class="secondary">Get the app</button></div> |
| 150 | <div class="row"><button id="welcome-web" class="secondary">Continue in the browser</button></div> |
| 151 | <p id="welcome-caveat" class="welcome-caveat"> |
| 152 | Joining in the browser keeps your access in this browser only — if it's |
| 153 | cleared or lost, you'll need a new invite. |
| 154 | </p> |
| 155 | </template> |
| 156 | |
| 157 | <script type="module" src="/static/join.js"></script> |
| 158 | |