the statusphere demo reworked into a vite/react app in a monorepo

Compare changes

Choose any two refs to compare.

Changed files
+70 -12
.tangled
workflows
packages
appview
client
src
components
pages
+27
.tangled/workflows/deploy.yml
··· 1 + # Set the following secrets in your repo's pipeline settings: 2 + # RAILWAY_TOKEN 3 + # RAILWAY_SERVICE_ID 4 + 5 + when: 6 + - event: ["push"] 7 + branch: ["main"] 8 + 9 + engine: "nixery" 10 + 11 + dependencies: 12 + nixpkgs: 13 + - rustup 14 + - gcc 15 + 16 + steps: 17 + - name: Install Rust toolchain 18 + command: rustup default stable 19 + 20 + - name: Install Railway CLI 21 + command: cargo install railwayapp --locked 22 + 23 + - name: Link `railway` executable 24 + command: ln -s /tangled/home/.cargo/bin/railway /bin/railway 25 + 26 + - name: Deploy to Railway 27 + command: railway up --ci --service=$RAILWAY_SERVICE_ID
+1 -1
packages/appview/README.md
··· 55 55 56 56 ## API Endpoints 57 57 58 - - `GET /client-metadata.json` - OAuth client metadata 58 + - `GET /oauth-client-metadata.json` - OAuth client metadata 59 59 - `GET /oauth/callback` - OAuth callback endpoint 60 60 - `POST /login` - Login with handle 61 61 - `POST /logout` - Logout current user
+15 -2
packages/appview/src/api/oauth.ts
··· 9 9 const router = express.Router() 10 10 11 11 // OAuth metadata 12 - router.get('/client-metadata.json', (_req, res) => { 12 + router.get('/oauth-client-metadata.json', (_req, res) => { 13 13 res.json(ctx.oauthClient.clientMetadata) 14 14 }) 15 15 ··· 51 51 router.post('/oauth/initiate', async (req, res) => { 52 52 // Validate 53 53 const handle = req.body?.handle 54 - if (typeof handle !== 'string' || !isValidHandle(handle)) { 54 + if ( 55 + typeof handle !== 'string' || 56 + !(isValidHandle(handle) || isValidUrl(handle)) 57 + ) { 55 58 res.status(400).json({ error: 'Invalid handle' }) 56 59 return 57 60 } ··· 81 84 82 85 return router 83 86 } 87 + 88 + function isValidUrl(url: string): boolean { 89 + try { 90 + const urlp = new URL(url) 91 + // http or https 92 + return urlp.protocol === 'http:' || urlp.protocol === 'https:' 93 + } catch (error) { 94 + return false 95 + } 96 + }
+1 -1
packages/appview/src/auth/client.ts
··· 17 17 clientMetadata: { 18 18 client_name: 'Statusphere React App', 19 19 client_id: publicUrl 20 - ? `${url}/client-metadata.json` 20 + ? `${url}/oauth-client-metadata.json` 21 21 : `http://localhost?redirect_uri=${enc(`${url}/oauth/callback`)}&scope=${enc('atproto transition:generic')}`, 22 22 client_uri: url, 23 23 redirect_uris: [`${url}/oauth/callback`],
+1 -1
packages/appview/src/index.ts
··· 120 120 async close() { 121 121 this.ctx.logger.info('sigint received, shutting down') 122 122 await this.ctx.ingester.destroy() 123 - return new Promise<void>((resolve) => { 123 + await new Promise<void>((resolve) => { 124 124 this.server.close(() => { 125 125 this.ctx.logger.info('server closed') 126 126 resolve()
+7 -1
packages/appview/src/ingestors/jetstream.ts
··· 93 93 private cursor?: number 94 94 private ws?: WebSocket 95 95 private isStarted = false 96 + private isDestroyed = false 96 97 private wantedCollections: string[] 97 98 98 99 constructor({ ··· 133 134 start() { 134 135 if (this.isStarted) return 135 136 this.isStarted = true 137 + this.isDestroyed = false 136 138 this.ws = new WebSocket(this.constructUrlWithQuery()) 137 139 138 140 this.ws.on('open', () => { ··· 159 161 }) 160 162 161 163 this.ws.on('close', (code, reason) => { 162 - this.logger.error(`Jetstream closed. Code: ${code}, Reason: ${reason}`) 164 + if (!this.isDestroyed) { 165 + this.logger.error(`Jetstream closed. Code: ${code}, Reason: ${reason}`) 166 + } 163 167 this.isStarted = false 164 168 }) 165 169 } 166 170 167 171 destroy() { 168 172 if (this.ws) { 173 + this.isDestroyed = true 169 174 this.ws.close() 170 175 this.isStarted = false 176 + this.logger.info('jetstream destroyed gracefully') 171 177 } 172 178 } 173 179 }
+1 -1
packages/appview/src/lib/env.ts
··· 15 15 COOKIE_SECRET: str({ devDefault: '0'.repeat(32) }), 16 16 SERVICE_DID: str({ default: undefined }), 17 17 PUBLIC_URL: str({ devDefault: '' }), 18 - JETSTREAM_INSTANCE: str({ default: 'wss://jetstream.mozzius.dev' }), 18 + JETSTREAM_INSTANCE: str({ default: 'wss://jetstream2.us-east.bsky.network' }), 19 19 })
+4 -1
packages/appview/src/lib/hydrate.ts
··· 7 7 import { AppContext } from '#/context' 8 8 import { Status } from '#/db' 9 9 10 + const INVALID_HANDLE = 'handle.invalid' 11 + 10 12 export async function statusToStatusView( 11 13 status: Status, 12 14 ctx: AppContext, ··· 19 21 did: status.authorDid, 20 22 handle: await ctx.resolver 21 23 .resolveDidToHandle(status.authorDid) 22 - .catch(() => 'invalid.handle'), 24 + .then((handle) => (handle.startsWith('did:') ? INVALID_HANDLE : handle)) 25 + .catch(() => INVALID_HANDLE), 23 26 }, 24 27 } 25 28 }
+7 -2
packages/client/index.html
··· 1 - <!DOCTYPE html> 1 + <!doctype html> 2 2 <html lang="en"> 3 3 <head> 4 4 <meta charset="UTF-8" /> 5 5 <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 6 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 7 <title>Statusphere React</title> 8 + <script 9 + defer 10 + data-domain="statusphere.mozzius.dev" 11 + src="https://plausible.mozzius.dev/js/script.js" 12 + ></script> 8 13 </head> 9 14 <body> 10 15 <div id="root"></div> 11 16 <script type="module" src="/src/main.tsx"></script> 12 17 </body> 13 - </html> 18 + </html>
+1 -1
packages/client/src/components/Header.tsx
··· 31 31 <img 32 32 src={user.profile.avatar} 33 33 alt={user.profile.displayName || user.profile.handle} 34 - className="w-8 h-8 rounded-full" 34 + className="w-8 h-8 rounded-full text-transparent" 35 35 /> 36 36 ) : ( 37 37 <div className="w-8 h-8 bg-gray-200 dark:bg-gray-700 rounded-full"></div>
+5 -1
packages/client/src/pages/LoginPage.tsx
··· 49 49 <Header /> 50 50 51 51 <div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm max-w-md mx-auto w-full"> 52 - <h2 className="text-xl font-semibold mb-4">Login with your handle</h2> 52 + <h2 className="text-xl font-semibold mb-4">Login with ATProto</h2> 53 53 54 54 {error && ( 55 55 <div className="text-red-500 mb-4 p-2 bg-red-50 dark:bg-red-950 dark:bg-opacity-30 rounded-md"> ··· 74 74 disabled={pending} 75 75 className="w-full p-3 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 transition-colors" 76 76 /> 77 + <p className="text-gray-400 dark:text-gray-500 text-sm mt-2"> 78 + You can also enter an AT Protocol PDS URL, i.e.{' '} 79 + <span className="whitespace-nowrap">https://bsky.social</span> 80 + </p> 77 81 </div> 78 82 79 83 <button