what is this, almost useable?

Changed files
+141 -21
atproto-notifications
server
+94 -2
atproto-notifications/package-lock.json
··· 13 13 "lexicons": "file:../lexicons", 14 14 "psl": "^1.15.0", 15 15 "react": "^19.1.0", 16 - "react-dom": "^19.1.0" 16 + "react-dom": "^19.1.0", 17 + "react-time-ago": "^7.3.3" 17 18 }, 18 19 "devDependencies": { 19 20 "@eslint/js": "^9.29.0", ··· 2580 2581 "dev": true, 2581 2582 "license": "ISC" 2582 2583 }, 2584 + "node_modules/javascript-time-ago": { 2585 + "version": "2.5.11", 2586 + "resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.5.11.tgz", 2587 + "integrity": "sha512-Zeyf5R7oM1fSMW9zsU3YgAYwE0bimEeF54Udn2ixGd8PUwu+z1Yc5t4Y8YScJDMHD6uCx6giLt3VJR5K4CMwbg==", 2588 + "license": "MIT", 2589 + "peer": true, 2590 + "dependencies": { 2591 + "relative-time-format": "^1.1.6" 2592 + } 2593 + }, 2583 2594 "node_modules/js-tokens": { 2584 2595 "version": "4.0.0", 2585 2596 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 2586 2597 "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 2587 - "dev": true, 2588 2598 "license": "MIT" 2589 2599 }, 2590 2600 "node_modules/js-yaml": { ··· 2698 2708 "dev": true, 2699 2709 "license": "MIT" 2700 2710 }, 2711 + "node_modules/loose-envify": { 2712 + "version": "1.4.0", 2713 + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 2714 + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 2715 + "license": "MIT", 2716 + "dependencies": { 2717 + "js-tokens": "^3.0.0 || ^4.0.0" 2718 + }, 2719 + "bin": { 2720 + "loose-envify": "cli.js" 2721 + } 2722 + }, 2701 2723 "node_modules/lru-cache": { 2702 2724 "version": "5.1.1", 2703 2725 "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", ··· 2708 2730 "yallist": "^3.0.2" 2709 2731 } 2710 2732 }, 2733 + "node_modules/memoize-one": { 2734 + "version": "6.0.0", 2735 + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", 2736 + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", 2737 + "license": "MIT" 2738 + }, 2711 2739 "node_modules/merge2": { 2712 2740 "version": "1.4.1", 2713 2741 "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", ··· 2785 2813 "dev": true, 2786 2814 "license": "MIT" 2787 2815 }, 2816 + "node_modules/object-assign": { 2817 + "version": "4.1.1", 2818 + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 2819 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 2820 + "license": "MIT", 2821 + "engines": { 2822 + "node": ">=0.10.0" 2823 + } 2824 + }, 2788 2825 "node_modules/optionator": { 2789 2826 "version": "0.9.4", 2790 2827 "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", ··· 2868 2905 "node": ">=8" 2869 2906 } 2870 2907 }, 2908 + "node_modules/performance-now": { 2909 + "version": "2.1.0", 2910 + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 2911 + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", 2912 + "license": "MIT" 2913 + }, 2871 2914 "node_modules/picocolors": { 2872 2915 "version": "1.1.1", 2873 2916 "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", ··· 2927 2970 "node": ">= 0.8.0" 2928 2971 } 2929 2972 }, 2973 + "node_modules/prop-types": { 2974 + "version": "15.8.1", 2975 + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", 2976 + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", 2977 + "license": "MIT", 2978 + "dependencies": { 2979 + "loose-envify": "^1.4.0", 2980 + "object-assign": "^4.1.1", 2981 + "react-is": "^16.13.1" 2982 + } 2983 + }, 2930 2984 "node_modules/psl": { 2931 2985 "version": "1.15.0", 2932 2986 "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", ··· 2969 3023 ], 2970 3024 "license": "MIT" 2971 3025 }, 3026 + "node_modules/raf": { 3027 + "version": "3.4.1", 3028 + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", 3029 + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", 3030 + "license": "MIT", 3031 + "dependencies": { 3032 + "performance-now": "^2.1.0" 3033 + } 3034 + }, 2972 3035 "node_modules/react": { 2973 3036 "version": "19.1.0", 2974 3037 "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", ··· 2989 3052 "peerDependencies": { 2990 3053 "react": "^19.1.0" 2991 3054 } 3055 + }, 3056 + "node_modules/react-is": { 3057 + "version": "16.13.1", 3058 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", 3059 + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", 3060 + "license": "MIT" 2992 3061 }, 2993 3062 "node_modules/react-refresh": { 2994 3063 "version": "0.17.0", ··· 2999 3068 "engines": { 3000 3069 "node": ">=0.10.0" 3001 3070 } 3071 + }, 3072 + "node_modules/react-time-ago": { 3073 + "version": "7.3.3", 3074 + "resolved": "https://registry.npmjs.org/react-time-ago/-/react-time-ago-7.3.3.tgz", 3075 + "integrity": "sha512-5kh2Kuu/UhHzcZrGvf3GUrF2d+IXjkIXif5MR2iDWIfSqQuBW27/ejN/tmzJBRyPiryYTgbDIG6AZFJ4RW3yfw==", 3076 + "license": "MIT", 3077 + "dependencies": { 3078 + "memoize-one": "^6.0.0", 3079 + "prop-types": "^15.8.1", 3080 + "raf": "^3.4.1" 3081 + }, 3082 + "peerDependencies": { 3083 + "javascript-time-ago": "^2.3.7", 3084 + "react": ">=0.16.8", 3085 + "react-dom": ">=0.16.8" 3086 + } 3087 + }, 3088 + "node_modules/relative-time-format": { 3089 + "version": "1.1.6", 3090 + "resolved": "https://registry.npmjs.org/relative-time-format/-/relative-time-format-1.1.6.tgz", 3091 + "integrity": "sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==", 3092 + "license": "MIT", 3093 + "peer": true 3002 3094 }, 3003 3095 "node_modules/resolve-from": { 3004 3096 "version": "4.0.0",
+2 -1
atproto-notifications/package.json
··· 15 15 "lexicons": "file:../lexicons", 16 16 "psl": "^1.15.0", 17 17 "react": "^19.1.0", 18 - "react-dom": "^19.1.0" 18 + "react-dom": "^19.1.0", 19 + "react-time-ago": "^7.3.3" 19 20 }, 20 21 "devDependencies": { 21 22 "@eslint/js": "^9.29.0",
+8 -1
atproto-notifications/src/components/Notification.css
··· 4 4 padding: 0.75rem; 5 5 border: 0.5px solid hsla(0, 0%, 50%, 0.3); 6 6 border-width: 0.5px 0; 7 + box-sizing: border-box; 8 + display: flex; 9 + justify-content: space-between; 7 10 } 8 11 a.notification { 9 - display: block; 10 12 font: inherit; 11 13 color: inherit; 12 14 } ··· 23 25 .handle { 24 26 color: skyblue; 25 27 } 28 + 29 + .notification-when { 30 + font-size: 0.8rem; 31 + opacity: 0.667; 32 + }
+22 -14
atproto-notifications/src/components/Notification.tsx
··· 1 + import ReactTimeAgo from 'react-time-ago'; 1 2 import psl from 'psl'; 2 3 import lexicons from 'lexicons'; 3 4 import { resolveDid } from '../atproto/resolve'; ··· 5 6 6 7 import './Notification.css'; 7 8 8 - export function Notification({ app, group, source, source_record, source_did, subject }) { 9 + export function Notification({ app, group, source, source_record, source_did, subject, timestamp }) { 9 10 10 11 // TODO: clean up / move this to lexicons package? 11 12 let title = source; ··· 25 26 26 27 const contents = ( 27 28 <> 28 - {icon && ( 29 - <img className="app-icon" src={icon} title={appName ?? app} alt="" /> 30 - )} 31 - {title} from 32 - {' '} 33 - {source_did ? ( 34 - <Fetch 35 - using={resolveDid} 36 - args={[source_did]} 37 - ok={handle => <span className="handle">@{handle}</span>} 38 - /> 39 - ) : ( 40 - source_record 29 + <div className="notification-info"> 30 + {icon && ( 31 + <img className="app-icon" src={icon} title={appName ?? app} alt="" /> 32 + )} 33 + {title} from 34 + {' '} 35 + {source_did ? ( 36 + <Fetch 37 + using={resolveDid} 38 + args={[source_did]} 39 + ok={handle => <span className="handle">@{handle}</span>} 40 + /> 41 + ) : ( 42 + source_record 43 + )} 44 + </div> 45 + {timestamp && ( 46 + <div className="notification-when"> 47 + <ReactTimeAgo date={new Date(timestamp)} locale="en-US"/> 48 + </div> 41 49 )} 42 50 </> 43 51 );
+5 -1
atproto-notifications/src/main.tsx
··· 7 7 <StrictMode> 8 8 <App /> 9 9 </StrictMode>, 10 - ) 10 + ); 11 + 12 + import TimeAgo from 'javascript-time-ago' 13 + import en from 'javascript-time-ago/locale/en' 14 + TimeAgo.addDefaultLocale(en)
+2 -1
atproto-notifications/src/service-worker.ts
··· 8 8 self.addEventListener('notificationclick', handleNotificationClick); 9 9 10 10 async function handlePush(ev) { 11 - const { subject, source, source_record } = ev.data.json(); 11 + const { subject, source, source_record, timestamp } = ev.data.json(); 12 12 let group; 13 13 let app; 14 14 let appPrefix; ··· 45 45 46 46 try { 47 47 await insertNotification({ 48 + timestamp, 48 49 subject, 49 50 source_record, 50 51 source_did,
+8 -1
server/index.js
··· 8 8 const cookieSig = require('cookie-signature'); 9 9 const webpush = require('web-push'); 10 10 11 + // kind of silly but right now there's no way to tell spacedust that we want an alive connection 12 + // but don't want the notification firehose (everything filtered out) 13 + // so... the final filter is an absolute on this fake did, effectively filtering all notifs. 14 + // (this is only used when there are no subscribers registered) 11 15 const DUMMY_DID = 'did:plc:zzzzzzzzzzzzzzzzzzzzzzzz'; 12 16 13 17 const CORS_PERMISSIVE = req => ({ ··· 58 62 return; 59 63 } 60 64 const { link: { subject, source, source_record } } = data; 65 + const timestamp = +new Date(); 61 66 62 67 let did; 63 68 if (subject.startsWith('did:')) did = subject; ··· 72 77 73 78 const expiredSubs = []; 74 79 const now = new Date(); 80 + const payload = JSON.stringify({ subject, source, source_record, timestamp }); 81 + console.log('pl', payload); 75 82 for (const sub of subs.get(did) ?? []) { 76 83 try { 77 84 if (now - sub.t < 1500) { ··· 79 86 continue; 80 87 } 81 88 sub.t = now; 82 - await webpush.sendNotification(sub, JSON.stringify({ subject, source, source_record })); 89 + await webpush.sendNotification(sub, payload); 83 90 } catch (err) { 84 91 if (400 <= err.statusCode && err.statusCode < 500) { 85 92 expiredSubs.push(sub);