hello early page

Changed files
+122 -16
atproto-notifications
lexicons
server
+8 -3
atproto-notifications/src/components/Buttons.css
··· 1 - button { 1 + button, 2 + a.button { 2 3 border-radius: 0.5rem; 3 4 border: 1px solid transparent; 5 + color: inherit; 4 6 padding: 0.6em 1.2em; 5 7 font-size: 1em; 6 8 font-weight: 500; ··· 12 14 border-right-color: hsla(0, 0%, 0%, 0.3); 13 15 box-shadow: 0 42px 42px -42px inset #221828; 14 16 } 15 - button:hover { 17 + button:hover, 18 + a.button:hover { 16 19 border-color: #646cff; 17 20 } 18 21 button:focus, 19 - button:focus-visible { 22 + button:focus-visible, 23 + a.button:focus, 24 + a.button:focus-visible { 20 25 outline: 4px auto -webkit-focus-ring-color; 21 26 } 22 27
+1 -1
atproto-notifications/src/components/Fetch.tsx
··· 67 67 ); 68 68 } 69 69 70 - async function postJson(url, body, credentials) { 70 + export async function postJson(url, body, credentials) { 71 71 const opts = { 72 72 method: 'POST', 73 73 headers: {'Content-Type': 'applicaiton/json'},
+6 -5
atproto-notifications/src/components/SecretPassword.jsx
··· 38 38 }} 39 39 ok={() => ( 40 40 <> 41 - <p>That will do.</p> 41 + <p style={{ color: "#9f0" }}>Secret password accepted.</p> 42 42 <p> 43 - <button onClick={() => window.location.reload()}> 44 - Enter 45 - </button> 43 + {/* an <a> tag, not a <Link>, on purpose so we relaod for our role */} 44 + <a className="button" href="/early"> 45 + Continue 46 + </a> 46 47 </p> 47 48 </> 48 49 )} ··· 62 63 {' '} 63 64 {begun && ( 64 65 <button type="submit" className="subtle"> 65 - open sesame 66 + unlock 66 67 </button> 67 68 )} 68 69 </p>
+1 -1
atproto-notifications/src/components/setup/WithNotificationPermission.tsx
··· 15 15 if (currentPermission !== 'granted') { 16 16 return ( 17 17 <> 18 - <h3>Step 2: Allow notifications</h3> 18 + <h3>Final step: Allow notifications</h3> 19 19 <p>To show notifications we need permission:</p> 20 20 <p> 21 21 <button
+2
atproto-notifications/src/main.tsx
··· 5 5 import { App } from './App'; 6 6 import { Feed } from './pages/Feed'; 7 7 import { Admin } from './pages/Admin'; 8 + import { Early } from './pages/Early'; 8 9 9 10 createRoot(document.getElementById('root')!).render( 10 11 // <StrictMode> ··· 13 14 <Routes> 14 15 <Route index element={<Feed />} /> 15 16 <Route path="/admin" element={<Admin />} /> 17 + <Route path="/early" element={<Early />} /> 16 18 </Routes> 17 19 </App> 18 20 </BrowserRouter>
+5
atproto-notifications/src/pages/Early.css
··· 1 + .early { 2 + max-width: 36rem; 3 + text-align: left; 4 + margin: 0 auto; 5 + }
+57
atproto-notifications/src/pages/Early.tsx
··· 1 + import { useCallback, useState } from 'react'; 2 + import { postJson } from '../components/Fetch'; 3 + import './Early.css'; 4 + 5 + export function Early({ }) { 6 + const [pushCount, setPushCount] = useState(0); 7 + const [pushStatus, setPushStatus] = useState(null); 8 + 9 + const localTest = useCallback(() => { 10 + new Notification("Hello world!", { body: "This notification never left your browser" }); 11 + }); 12 + 13 + const pushTest = useCallback(async () => { 14 + setPushStatus(n => n + 1); 15 + setPushStatus('pending'); 16 + const host = import.meta.env.VITE_NOTIFICATIONS_HOST; 17 + const url = new URL('/push-test', host); 18 + try { 19 + await postJson(url, JSON.stringify(null), true); 20 + setPushStatus(null); 21 + } catch (e) { 22 + console.error('failed push test request', e); 23 + setPushStatus('failed'); 24 + } 25 + }); 26 + 27 + return ( 28 + <div className="early"> 29 + <h2>Hello!</h2> 30 + <p>Welcome to the early preview for the spacedust notifications demo, and since you're here early: thanks so much for supporting microcosm!</p> 31 + <p>A few things to keep in mind:</p> 32 + <ol> 33 + <li>This is a demo, not a polished product</li> 34 + <li>It has a lot of moving pieces, so things not always work</li> 35 + <li>Many features can easily be added! Some others can't! Make a request and let's see :)</li> 36 + <li>It's not a long-term committed part of microcosm <em>(yet)</em></li> 37 + </ol> 38 + <p>Sadly, it doesn't really work on mobile. On iOS you can hit "share" and "add to home screen" to get things eventually mostly set up, but push delivery will stop after a few minutes. Android people might have better luck?</p> 39 + <h3>Hello hello</h3> 40 + <p>With that out of the way, let's cover some basics.</p> 41 + <p> 42 + To see a test notification, <button onClick={localTest}>click on this</button>. This is a local-only test. 43 + </p> 44 + <p> 45 + <button 46 + disabled={pushStatus === 'pending'} 47 + onClick={pushTest} 48 + > 49 + Click here {pushCount > 0 && <>({pushCount})</>} 50 + </button> 51 + {' '} 52 + to see another. This one goes over Web Push. 53 + </p> 54 + {pushStatus === 'failed' && <p>uh oh, something went wrong requesting a web push</p>} 55 + </div> 56 + ); 57 + }
+9
lexicons/index.js
··· 1 1 export default { 2 + 'blue.microcosm': { 3 + name: 'microcosm', 4 + clients: [ 5 + {}, 6 + ], 7 + known_sources: { 8 + 'test.notification:hello': 'Hello spacedust!', 9 + }, 10 + }, 2 11 'app.bsky': { 3 12 name: 'Bluesky', 4 13 clients: [
+17 -3
server/api.js
··· 133 133 return gotIt(res); 134 134 }; 135 135 136 + const handlePushTest = async (db, user, res, push) => { 137 + const subscription = db.getSubBySession(user.session); 138 + const payload = JSON.stringify({ 139 + subject: user.did, 140 + source: 'blue.microcosm.test.notification:hello', 141 + source_record: `at://${user.did}/blue.microcosm.test.notification/test`, 142 + timestamp: +new Date(), 143 + }); 144 + await push(db, subscription, payload); 145 + return okBye(res); 146 + }; 147 + 136 148 const handleLogout = async (db, user, req, res, appSecret, updateSubs) => { 137 149 try { 138 150 db.deleteSub(user.session); ··· 152 164 const { secret_password } = JSON.parse(body); 153 165 const { did } = user; 154 166 const role = 'early'; 155 - console.log('going with', {did, role, secret_password}); 156 167 const updated = db.setRole({ did, role, secret_password }); 157 - console.log('updated?', updated); 158 168 if (updated) { 159 169 return okBye(res); 160 170 } else { ··· 226 236 } 227 237 } 228 238 229 - export const server = (secrets, jwks, allowedOrigin, whoamiHost, db, updateSubs, adminDid) => { 239 + export const server = (secrets, jwks, allowedOrigin, whoamiHost, db, updateSubs, push, adminDid) => { 230 240 const handler = (req, res) => { 231 241 // don't love this but whatever 232 242 const { pathname, searchParams } = new URL(`http://localhost${req.url}`); ··· 260 270 if (method === 'POST' && pathname === '/subscribe') { 261 271 if (!user || user.role === 'public') return forbidden(res); 262 272 return handleSubscribe(db, user, req, res, updateSubs); 273 + } 274 + if (method === 'POST' && pathname === '/push-test') { 275 + if (!user || user.role === 'public') return forbidden(res); 276 + return handlePushTest(db, user, res, push); 263 277 } 264 278 265 279 // admin required (just 404 for non-admin)
+13
server/db.js
··· 11 11 #stmt_insert_push_sub; 12 12 #stmt_get_all_sub_dids; 13 13 #stmt_get_push_subs; 14 + #stmt_get_push_sub; 14 15 #stmt_update_push_sub; 15 16 #stmt_delete_push_sub; 16 17 #stmt_get_push_info; ··· 76 77 as 'since_last_push' 77 78 from push_subs 78 79 where account_did = ?`); 80 + 81 + this.#stmt_get_push_sub = db.prepare( 82 + `select session, 83 + subscription, 84 + (julianday(CURRENT_TIMESTAMP) - julianday(last_push)) * 24 * 60 * 60 85 + as 'since_last_push' 86 + from push_subs 87 + where session = ?`); 79 88 80 89 this.#stmt_update_push_sub = db.prepare( 81 90 `update push_subs ··· 157 166 158 167 getSubsByDid(did) { 159 168 return this.#stmt_get_push_subs.all(did); 169 + } 170 + 171 + getSubBySession(session) { 172 + return this.#stmt_get_push_sub.get(session); 160 173 } 161 174 162 175 updateLastPush(session) {
+2 -2
server/index.js
··· 47 47 const db = new DB(dbFilename, initDb); 48 48 49 49 const spacedustHost = env.SPACEDUST_HOST ?? 'wss://spacedust.microcosm.blue'; 50 - const updateSubs = connectSpacedust(db, spacedustHost); 50 + const { updateSubs, push } = connectSpacedust(db, spacedustHost); 51 51 52 52 const host = env.HOST ?? 'localhost'; 53 53 const port = parseInt(env.PORT ?? 8000, 10); 54 54 55 55 const allowedOrigin = env.ALLOWED_ORIGIN ?? 'http://127.0.0.1:5173'; 56 56 57 - server(secrets, jwks, allowedOrigin, whoamiHost, db, updateSubs, adminDid).listen( 57 + server(secrets, jwks, allowedOrigin, whoamiHost, db, updateSubs, push, adminDid).listen( 58 58 port, 59 59 host, 60 60 () => console.log(`listening at http://${host}:${port} with allowed origin: ${allowedOrigin}`),
+1 -1
server/notifications.js
··· 146 146 restart(); 147 147 }; 148 148 149 - return updateSubs; 149 + return { updateSubs, push }; 150 150 };