demos for spacedust

service worker take 2 (yay atcute)

+149 -5
+3
atproto-notifications/.gitignore
··· 22 22 *.njsproj 23 23 *.sln 24 24 *.sw? 25 + 26 + # built files 27 + public/service-worker.js
+59
atproto-notifications/package-lock.json
··· 8 8 "name": "atproto-notifications", 9 9 "version": "0.0.0", 10 10 "dependencies": { 11 + "@atcute/identity-resolver": "^1.1.3", 11 12 "@uidotdev/usehooks": "^2.4.1", 12 13 "react": "^19.1.0", 13 14 "react-dom": "^19.1.0" ··· 38 39 }, 39 40 "engines": { 40 41 "node": ">=6.0.0" 42 + } 43 + }, 44 + "node_modules/@atcute/identity": { 45 + "version": "1.0.3", 46 + "resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-1.0.3.tgz", 47 + "integrity": "sha512-mNMxbKHFGys03A8JXKk0KfMBzdd0vrYMzZZWjpw1nYTs0+ea6bo5S1hwqVUZxHdo1gFHSe/t63jxQIF4yL9aKw==", 48 + "license": "0BSD", 49 + "peer": true, 50 + "dependencies": { 51 + "@atcute/lexicons": "^1.0.4", 52 + "@badrap/valita": "^0.4.5" 53 + } 54 + }, 55 + "node_modules/@atcute/identity-resolver": { 56 + "version": "1.1.3", 57 + "resolved": "https://registry.npmjs.org/@atcute/identity-resolver/-/identity-resolver-1.1.3.tgz", 58 + "integrity": "sha512-KZgGgg99CWaV7Df3+h3X/WMrDzTPQVfsaoIVbTNLx2B56BvCL2EmaxPSVw/7BFUJMZHlVU4rtoEB4lyvNyMswA==", 59 + "license": "MIT", 60 + "dependencies": { 61 + "@atcute/lexicons": "^1.0.4", 62 + "@atcute/util-fetch": "^1.0.1", 63 + "@badrap/valita": "^0.4.4" 64 + }, 65 + "peerDependencies": { 66 + "@atcute/identity": "^1.0.0" 67 + } 68 + }, 69 + "node_modules/@atcute/lexicons": { 70 + "version": "1.1.0", 71 + "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.1.0.tgz", 72 + "integrity": "sha512-LFqwnria78xLYb62Ri/+WwQpUTgZp2DuyolNGIIOV1dpiKhFFFh//nscHMA6IExFLQRqWDs3tTjy7zv0h3sf1Q==", 73 + "license": "0BSD", 74 + "dependencies": { 75 + "esm-env": "^1.2.2" 76 + } 77 + }, 78 + "node_modules/@atcute/util-fetch": { 79 + "version": "1.0.1", 80 + "resolved": "https://registry.npmjs.org/@atcute/util-fetch/-/util-fetch-1.0.1.tgz", 81 + "integrity": "sha512-Clc0E/5ufyGBVfYBUwWNlHONlZCoblSr4Ho50l1LhmRPGB1Wu/AQ9Sz+rsBg7fdaW/auve8ulmwhRhnX2cGRow==", 82 + "license": "MIT", 83 + "dependencies": { 84 + "@badrap/valita": "^0.4.2" 41 85 } 42 86 }, 43 87 "node_modules/@babel/code-frame": { ··· 320 364 }, 321 365 "engines": { 322 366 "node": ">=6.9.0" 367 + } 368 + }, 369 + "node_modules/@badrap/valita": { 370 + "version": "0.4.5", 371 + "resolved": "https://registry.npmjs.org/@badrap/valita/-/valita-0.4.5.tgz", 372 + "integrity": "sha512-4QwGbuhh/JesHRQj79mO/l37PvJj4l/tlAu7+S1n4h47qwaNpZ0WDvIwUGLYUsdi9uQ5UPpiG9wb1Wm3XUFBUQ==", 373 + "license": "MIT", 374 + "engines": { 375 + "node": ">= 18" 323 376 } 324 377 }, 325 378 "node_modules/@esbuild/aix-ppc64": { ··· 2181 2234 "funding": { 2182 2235 "url": "https://opencollective.com/eslint" 2183 2236 } 2237 + }, 2238 + "node_modules/esm-env": { 2239 + "version": "1.2.2", 2240 + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", 2241 + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", 2242 + "license": "MIT" 2184 2243 }, 2185 2244 "node_modules/espree": { 2186 2245 "version": "10.4.0",
+2 -1
atproto-notifications/package.json
··· 4 4 "version": "0.0.0", 5 5 "type": "module", 6 6 "scripts": { 7 - "dev": "vite", 7 + "dev": "vite --host 127.0.0.1", 8 8 "build": "tsc -b && vite build", 9 9 "lint": "eslint .", 10 10 "preview": "vite preview" 11 11 }, 12 12 "dependencies": { 13 + "@atcute/identity-resolver": "^1.1.3", 13 14 "@uidotdev/usehooks": "^2.4.1", 14 15 "react": "^19.1.0", 15 16 "react-dom": "^19.1.0"
+1 -2
atproto-notifications/src/App.tsx
··· 35 35 } 36 36 37 37 async function subscribeToPush() { 38 - const worker_url = new URL('service-worker.ts', import.meta.url); 39 - const registration = await navigator.serviceWorker.register(worker_url); 38 + const registration = await navigator.serviceWorker.register('/service-worker.js'); 40 39 41 40 const subscribeOptions = { 42 41 userVisibleOnly: true,
+51
atproto-notifications/src/atproto/resolve.ts
··· 1 + import { CompositeDidDocumentResolver, PlcDidDocumentResolver, WebDidDocumentResolver } from '@atcute/identity-resolver'; 2 + 3 + const docResolver = new CompositeDidDocumentResolver({ 4 + methods: { 5 + plc: new PlcDidDocumentResolver(), 6 + web: new WebDidDocumentResolver(), 7 + }, 8 + }); 9 + 10 + export async function resolveDid(did) { 11 + let doc; 12 + try { 13 + doc = await docResolver.resolve(did); 14 + } catch (err) { 15 + throw err; 16 + // if (err instanceof DocumentNotFoundError) { 17 + // // did returned no document 18 + // } 19 + // if (err instanceof UnsupportedDidMethodError) { 20 + // // resolver doesn't support did method (composite resolver) 21 + // } 22 + // if (err instanceof ImproperDidError) { 23 + // // resolver considers did as invalid (atproto did:web) 24 + // } 25 + // if (err instanceof FailedDocumentResolutionError) { 26 + // // document resolution had thrown something unexpected (fetch error) 27 + // } 28 + // if (err instanceof HandleResolutionError) { 29 + // // the errors above extend this class, so you can do a catch-all. 30 + // } 31 + } 32 + 33 + if (!(doc.alsoKnownAs && doc.alsoKnownAs.length >= 1)) { 34 + console.error('questionable doc', doc); 35 + throw new Error('doc missing aka'); 36 + } 37 + 38 + const aka = doc.alsoKnownAs[0]; 39 + if (!aka.startsWith('at://')) { 40 + console.error('questionable aka doesn\'t start with aka://', aka); 41 + throw new Error('aka not an at-uri'); 42 + } 43 + 44 + const handle = aka.slice('at://'.length); 45 + if (handle.length === 0) { 46 + console.error('empty handle? aka:', aka); 47 + throw new Error('empty handle'); 48 + } 49 + 50 + return handle; 51 + }
+13 -1
atproto-notifications/src/service-worker.ts
··· 1 + import { resolveDid } from './atproto/resolve'; 2 + 1 3 self.addEventListener('push', handlePush); 2 4 self.addEventListener('notificationclick', handleNotificationClick); 3 5 ··· 43 45 'app.bsky.feed.like:subject.uri': 'New like 💜', 44 46 }[source] ?? source; 45 47 48 + let handle = 'unknown'; 49 + if (source_record.startsWith('at://')) { 50 + const did = source_record.slice('at://'.length).split('/')[0]; 51 + try { 52 + handle = await resolveDid(did); 53 + } catch (err) { 54 + console.error('failed to get handle', err); 55 + } 56 + } 57 + 46 58 // const tag = 'simple-push-demo-notification-tag'; 47 59 // TODO: resubscribe to notifs to try to stay alive 48 60 ··· 67 79 68 80 const notification = self.registration.showNotification(title, { 69 81 icon, 70 - body: source_record, 82 + body: `from ${handle}`, 71 83 // actions: [ 72 84 // {'action': 'bsky', title: 'Bluesky'}, 73 85 // {'action': 'spacedust', title: 'All notifications'},
+20 -1
atproto-notifications/vite.config.ts
··· 1 1 import { defineConfig } from 'vite' 2 + import { join } from 'node:path'; 3 + import { buildSync } from 'esbuild'; 2 4 import react from '@vitejs/plugin-react' 3 5 6 + const buildServiceWorker = forProd => ({ 7 + apply: forProd ? 'build' : 'serve', 8 + enforce: 'pre', 9 + transformIndexHtml() { 10 + buildSync({ 11 + minify: true, 12 + bundle: true, 13 + entryPoints: [join(process.cwd(), 'src', 'service-worker.ts')], 14 + outfile: join(process.cwd(), forProd ? 'dist' : 'public', 'service-worker.js'), 15 + }); 16 + }, 17 + }); 18 + 4 19 // https://vite.dev/config/ 5 20 export default defineConfig({ 6 - plugins: [react()], 21 + plugins: [ 22 + buildServiceWorker(true), 23 + buildServiceWorker(false), 24 + react(), 25 + ], 7 26 })