tangled
alpha
login
or
join now
bad-example.com
/
spacedust-utils
6
fork
atom
demos for spacedust
6
fork
atom
overview
issues
pulls
pipelines
put notifications in indexeddb to show on page
bad-example.com
8 months ago
e916a0ff
8466f256
+142
-10
4 changed files
expand all
collapse all
unified
split
atproto-notifications
public
service-worker.js
src
App.tsx
components
Feed.tsx
server
index.js
+66
-4
atproto-notifications/public/service-worker.js
···
1
1
self.addEventListener('push', handlePush);
2
2
3
3
-
function handlePush(event) {
4
4
-
const { title, body } = event.data.json();
5
5
-
// const icon = '/images/icon.png';
3
3
+
const getDB = ((upgrade, v) => {
4
4
+
let instance;
5
5
+
return () => {
6
6
+
if (instance) return instance;
7
7
+
const req = indexedDB.open('atproto-notifs', v);
8
8
+
instance = new Promise((resolve, reject) => {
9
9
+
req.onerror = () => reject(req.error);
10
10
+
req.onupgradeneeded = () => upgrade(req.result);
11
11
+
req.onsuccess = () => resolve(req.result);
12
12
+
});
13
13
+
return instance;
14
14
+
};
15
15
+
})(function dbUpgrade(db) {
16
16
+
try {
17
17
+
db.deleteObjectStore('notifs');
18
18
+
} catch (e) {}
19
19
+
db.createObjectStore('notifs', {
20
20
+
key: 'id',
21
21
+
autoIncrement: true,
22
22
+
});
23
23
+
}, 2);
24
24
+
25
25
+
const push = async notif => {
26
26
+
const tx = (await getDB()).transaction('notifs', 'readwrite');
27
27
+
return new Promise((resolve, reject) => {
28
28
+
tx.oncomplete = resolve;
29
29
+
tx.onerror = () => reject(tx.error);
30
30
+
tx.objectStore('notifs').put(notif);
31
31
+
});
32
32
+
};
33
33
+
34
34
+
async function handlePush(event) {
35
35
+
const { subject, source, source_record } = event.data.json();
36
36
+
37
37
+
let icon;
38
38
+
if (source.startsWith('app.bsky')) icon = '/icons/app.bsky.png';
39
39
+
40
40
+
let title = {
41
41
+
'app.bsky.graph.follow:subject': 'New follow',
42
42
+
'app.bsky.feed.like:subject.uri': 'New like 💜',
43
43
+
}[source] ?? source;
44
44
+
6
45
// const tag = 'simple-push-demo-notification-tag';
7
7
-
event.waitUntil(self.registration.showNotification(title, { body }));
8
46
// TODO: resubscribe to notifs to try to stay alive
47
47
+
48
48
+
let db;
49
49
+
try {
50
50
+
db = await getDB();
51
51
+
} catch (e) {
52
52
+
console.error('oh no', e);
53
53
+
throw e;
54
54
+
}
55
55
+
db.onerror = e => {
56
56
+
console.error('db errored', e);
57
57
+
};
58
58
+
59
59
+
try {
60
60
+
await push({ subject, source, source_record });
61
61
+
} catch (e) {
62
62
+
console.error('uh oh', e);
63
63
+
}
64
64
+
65
65
+
new BroadcastChannel('notif').postMessage('heyyy');
66
66
+
67
67
+
event.waitUntil(self.registration.showNotification(title, {
68
68
+
icon,
69
69
+
body: source_record,
70
70
+
}));
9
71
}
+2
atproto-notifications/src/App.tsx
···
2
2
import { useLocalStorage } from "@uidotdev/usehooks";
3
3
import { HostContext } from './context'
4
4
import { WhoAmI } from './components/WhoAmI';
5
5
+
import { Feed } from './components/Feed';
5
6
import { urlBase64ToUint8Array } from './utils';
6
7
import './App.css'
7
8
···
120
121
@{user.handle}
121
122
<button onClick={() => setUser(null)}>×</button>
122
123
</p>
124
124
+
<Feed />
123
125
</>
124
126
);
125
127
}
+71
atproto-notifications/src/components/Feed.tsx
···
1
1
+
import { useEffect, useState } from 'react';
2
2
+
3
3
+
const getDB = ((upgrade, v) => {
4
4
+
let instance;
5
5
+
return () => {
6
6
+
if (instance) return instance;
7
7
+
const req = indexedDB.open('atproto-notifs', v);
8
8
+
instance = new Promise((resolve, reject) => {
9
9
+
req.onerror = () => reject(req.error);
10
10
+
req.onupgradeneeded = () => upgrade(req.result);
11
11
+
req.onsuccess = () => resolve(req.result);
12
12
+
});
13
13
+
return instance;
14
14
+
};
15
15
+
})(function dbUpgrade(db) {
16
16
+
try {
17
17
+
db.deleteObjectStore('notifs');
18
18
+
} catch (e) {}
19
19
+
db.createObjectStore('notifs', {
20
20
+
key: 'id',
21
21
+
autoIncrement: true,
22
22
+
});
23
23
+
}, 2);
24
24
+
25
25
+
const getNotifs = async (limit = 30) => {
26
26
+
let res = [];
27
27
+
const oc = (await getDB())
28
28
+
.transaction(['notifs'])
29
29
+
.objectStore('notifs')
30
30
+
.openCursor(undefined, 'prev');
31
31
+
return new Promise((resolve, reject) => {
32
32
+
oc.onerror = () => reject(oc.error);
33
33
+
oc.onsuccess = ev => {
34
34
+
const cursor = event.target.result;
35
35
+
if (cursor) {
36
36
+
res.push([cursor.key, cursor.value]);
37
37
+
if (res.length < limit) cursor.continue();
38
38
+
else resolve(res);
39
39
+
} else {
40
40
+
resolve(res);
41
41
+
}
42
42
+
}
43
43
+
});
44
44
+
};
45
45
+
46
46
+
export function Feed() {
47
47
+
48
48
+
// for now, we just increment a counter when a new notif comes in, which forces a re-render
49
49
+
const [inc, setInc] = useState(0);
50
50
+
useEffect(() => {
51
51
+
const handleMessage = () => setInc(n => n + 1);
52
52
+
const chan = new BroadcastChannel('notif');
53
53
+
chan.addEventListener('message', handleMessage);
54
54
+
return () => chan.removeEventListener('message', handleMessage);
55
55
+
});
56
56
+
57
57
+
// semi-gross way to just pull out all the events so we can see them
58
58
+
// this could be combined with the broadcast thing above, but for now just chain deps
59
59
+
const [feed, setFeed] = useState([]);
60
60
+
useEffect(() => {
61
61
+
(async () => setFeed((await getNotifs())))();
62
62
+
}, [inc]);
63
63
+
64
64
+
if (feed.length === 0) {
65
65
+
return 'no notifications loaded';
66
66
+
}
67
67
+
return feed.map(([k, n]) => (
68
68
+
<p key={k}>{k}: {n.source} ({n.source_record}) <code>{JSON.stringify(n)}</code></p>
69
69
+
));
70
70
+
71
71
+
}
+3
-6
server/index.js
···
70
70
}
71
71
72
72
const expiredSubs = [];
73
73
-
for (const sub of subs.get(did) ?? []) {
74
74
-
const title = `new ${source}`;
75
75
-
const body = `from ${source_record}`;
76
76
-
try {
77
77
-
await webpush.sendNotification(sub, JSON.stringify({ title, body }));
73
73
+
for (const sub of subs.get(did) ?? []) { try {
74
74
+
await webpush.sendNotification(sub, JSON.stringify({ subject, source, source_record }));
78
75
} catch (err) {
79
76
if (400 <= err.statusCode && err.statusCode < 500) {
80
77
expiredSubs.push(sub);
···
206
203
207
204
const body = await getRequesBody(req);
208
205
const { sub } = JSON.parse(body);
209
209
-
// addSub('did:plc:z72i7hdynmk6r22z27h6tvur', sub); // DELETEME @bsky.app (DEBUG)
206
206
+
addSub('did:plc:z72i7hdynmk6r22z27h6tvur', sub); // DELETEME @bsky.app (DEBUG)
210
207
addSub(did, sub);
211
208
res.setHeader('Content-Type', 'application/json');
212
209
res.writeHead(201);