+94
-2
atproto-notifications/package-lock.json
+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
+2
-1
atproto-notifications/package.json
+8
-1
atproto-notifications/src/components/Notification.css
+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
+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
+5
-1
atproto-notifications/src/main.tsx
+2
-1
atproto-notifications/src/service-worker.ts
+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
-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);