tangled
alpha
login
or
join now
bad-example.com
/
spacedust-utils
6
fork
atom
demos for spacedust
6
fork
atom
overview
issues
pulls
pipelines
more db in the indexeddb
bad-example.com
8 months ago
87879b07
a5743b99
+127
-97
4 changed files
expand all
collapse all
unified
split
atproto-notifications
src
atproto
resolve.ts
components
Feed.tsx
db.ts
service-worker.ts
+2
atproto-notifications/src/atproto/resolve.ts
···
47
47
throw new Error('empty handle');
48
48
}
49
49
50
50
+
// TODO: do we need to resolve back the other way to verify?
51
51
+
50
52
return handle;
51
53
}
+2
-44
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
-
};
2
2
+
import { getNotifications } from '../db';
45
3
46
4
export function Feed() {
47
5
···
58
16
// this could be combined with the broadcast thing above, but for now just chain deps
59
17
const [feed, setFeed] = useState([]);
60
18
useEffect(() => {
61
61
-
(async () => setFeed((await getNotifs())))();
19
19
+
(async () => setFeed((await getNotifications())))();
62
20
}, [inc]);
63
21
64
22
if (feed.length === 0) {
+107
atproto-notifications/src/db.ts
···
1
1
+
const NOTIFICATIONS = 'notifications';
2
2
+
const SECONDARIES = ['all', 'source', 'group', 'app'];
3
3
+
4
4
+
export const getDB = ((upgrade, v) => {
5
5
+
let instance;
6
6
+
return () => {
7
7
+
if (instance) return instance;
8
8
+
const req = indexedDB.open('atproto-notifs', v);
9
9
+
instance = new Promise((resolve, reject) => {
10
10
+
req.onerror = () => reject(req.error);
11
11
+
req.onupgradeneeded = () => upgrade(req.result);
12
12
+
req.onsuccess = () => resolve(req.result);
13
13
+
});
14
14
+
return instance;
15
15
+
};
16
16
+
})(function dbUpgrade(db) {
17
17
+
18
18
+
// primary store for notifications
19
19
+
try {
20
20
+
// upgrade is a reset: entirely remove the store (ignore errors if it didn't exist)
21
21
+
db.deleteObjectStore('notifs');
22
22
+
} catch (e) {}
23
23
+
const notifStore = db.createObjectStore(NOTIFICATIONS, {
24
24
+
key: 'id',
25
25
+
autoIncrement: true,
26
26
+
});
27
27
+
// subject prob doesn't need an index, could just query constellation
28
28
+
notifStore.createIndex('subject', 'subject', { unique: false });
29
29
+
// specific notification (not unique bc spacedust doens't emit deletes yet)
30
30
+
notifStore.createIndex('source_record', 'source_record', { unique: false });
31
31
+
// filter by source user of notifications because why not
32
32
+
notifStore.createIndex('source_did', 'source_did', { unique: false });
33
33
+
// notifications of an exact type
34
34
+
notifStore.createIndex('source', 'source', { unique: false });
35
35
+
// by nsid group
36
36
+
notifStore.createIndex('group', 'group', { unique: false });
37
37
+
// by nsid tld+1
38
38
+
notifStore.createIndex('app', 'app', { unique: false });
39
39
+
40
40
+
// secondary indexes: notification counts
41
41
+
for (const secondary of SECONDARIES) {
42
42
+
try {
43
43
+
// upgrade is hard reset
44
44
+
db.deleteObjectStore(secondary);
45
45
+
} catch (e) {}
46
46
+
const store = db.createObjectStore(secondary, {
47
47
+
key: 'k',
48
48
+
});
49
49
+
store.createIndex('total', 'total', { unique: false });
50
50
+
store.createIndex('unread', 'unread', { unique: false });
51
51
+
}
52
52
+
53
53
+
}, 3);
54
54
+
55
55
+
export async function insertNotification(notif: {
56
56
+
subject: String,
57
57
+
source_record: String,
58
58
+
source_did: String,
59
59
+
source: String,
60
60
+
group: String,
61
61
+
app: String,
62
62
+
}) {
63
63
+
const db = await getDB();
64
64
+
const tx = db.transaction([NOTIFICATIONS, ...SECONDARIES], 'readwrite');
65
65
+
66
66
+
// 1. insert the actual notification
67
67
+
tx.objectStore(NOTIFICATIONS).put(notif);
68
68
+
69
69
+
// 2. update all secondary counts
70
70
+
for (const secondary of SECONDARIES) {
71
71
+
const store = tx.objectStore(secondary);
72
72
+
const key = secondary === 'all' ? 'all' : notif[secondary];
73
73
+
store.get(key).onsuccess = ev => {
74
74
+
let count = ev.target.result ?? { total: 0, unread: 0 };
75
75
+
count.total += 1;
76
76
+
count.unread += 1;
77
77
+
store.put(count, key);
78
78
+
};
79
79
+
const req = tx.objectStore(s).get(s === 'all' ? s : notif[s]);
80
80
+
}
81
81
+
82
82
+
return new Promise((resolve, reject) => {
83
83
+
tx.onerror = () => reject(tx.error);
84
84
+
tx.oncomplete = resolve;
85
85
+
});
86
86
+
}
87
87
+
88
88
+
export async function getNotifications(limit = 30) {
89
89
+
let res = [];
90
90
+
const oc = (await getDB())
91
91
+
.transaction([NOTIFICATIONS])
92
92
+
.objectStore(NOTIFICATIONS)
93
93
+
.openCursor(undefined, 'prev');
94
94
+
return new Promise((resolve, reject) => {
95
95
+
oc.onerror = () => reject(oc.error);
96
96
+
oc.onsuccess = ev => {
97
97
+
const cursor = event.target.result;
98
98
+
if (cursor) {
99
99
+
res.push([cursor.key, cursor.value]);
100
100
+
if (res.length < limit) cursor.continue();
101
101
+
else resolve(res);
102
102
+
} else {
103
103
+
resolve(res);
104
104
+
}
105
105
+
}
106
106
+
});
107
107
+
}
+16
-53
atproto-notifications/src/service-worker.ts
···
1
1
import psl from 'psl';
2
2
import { resolveDid } from './atproto/resolve';
3
3
+
import { insertNotification } from './db';
3
4
4
5
self.addEventListener('push', handlePush);
5
6
self.addEventListener('notificationclick', handleNotificationClick);
6
7
7
7
-
const getDB = ((upgrade, v) => {
8
8
-
let instance;
9
9
-
return () => {
10
10
-
if (instance) return instance;
11
11
-
const req = indexedDB.open('atproto-notifs', v);
12
12
-
instance = new Promise((resolve, reject) => {
13
13
-
req.onerror = () => reject(req.error);
14
14
-
req.onupgradeneeded = () => upgrade(req.result);
15
15
-
req.onsuccess = () => resolve(req.result);
16
16
-
});
17
17
-
return instance;
18
18
-
};
19
19
-
})(function dbUpgrade(db) {
20
20
-
try {
21
21
-
db.deleteObjectStore('notifs');
22
22
-
} catch (e) {}
23
23
-
db.createObjectStore('notifs', {
24
24
-
key: 'id',
25
25
-
autoIncrement: true,
26
26
-
});
27
27
-
}, 2);
28
28
-
29
29
-
const push = async notif => {
30
30
-
const tx = (await getDB()).transaction('notifs', 'readwrite');
31
31
-
return new Promise((resolve, reject) => {
32
32
-
tx.oncomplete = resolve;
33
33
-
tx.onerror = () => reject(tx.error);
34
34
-
tx.objectStore('notifs').put(notif);
35
35
-
});
36
36
-
};
37
37
-
38
8
async function handlePush(ev) {
39
9
const { subject, source, source_record } = ev.data.json();
40
10
···
47
17
}[source] ?? source;
48
18
49
19
let handle = 'unknown';
20
20
+
let source_did;
50
21
if (source_record.startsWith('at://')) {
51
51
-
const did = source_record.slice('at://'.length).split('/')[0];
22
22
+
source_did = source_record.slice('at://'.length).split('/')[0];
52
23
try {
53
53
-
handle = await resolveDid(did);
24
24
+
handle = await resolveDid(source_did);
54
25
} catch (err) {
55
26
console.error('failed to get handle', err);
56
27
}
···
60
31
// TODO: resubscribe to notifs to try to stay alive
61
32
62
33
let group;
63
63
-
let domain;
34
34
+
let app;
64
35
try {
65
36
const [nsid, ...rp] = source.split(':');
66
37
const parts = nsid.split('.');
67
38
group = parts.slice(0, parts.length - 1).join('.') ?? 'unknown';
68
39
const unreversed = parts.toReversed().join('.');
69
69
-
domain = psl.parse(unreversed)?.domain ?? 'unknown';
40
40
+
app = psl.parse(unreversed)?.domain ?? 'unknown';
70
41
} catch (e) {
71
71
-
console.error('getting top app domain failed', e);
42
42
+
console.error('getting top app failed', e);
72
43
}
73
44
74
74
-
let db;
75
45
try {
76
76
-
db = await getDB();
46
46
+
await insertNotification({
47
47
+
subject,
48
48
+
source_record,
49
49
+
source_did,
50
50
+
source,
51
51
+
group,
52
52
+
app,
53
53
+
});
77
54
} catch (e) {
78
55
console.error('oh no', e);
79
79
-
throw e;
80
80
-
}
81
81
-
db.onerror = e => {
82
82
-
console.error('db errored', e);
83
83
-
};
84
84
-
85
85
-
try {
86
86
-
await push({ subject, source, source_record });
87
87
-
} catch (e) {
88
88
-
console.error('uh oh', e);
89
56
}
90
57
91
58
new BroadcastChannel('notif').postMessage('heyyy');
92
59
93
60
const notification = self.registration.showNotification(title, {
94
61
icon,
95
95
-
body: `from ${handle} on ${domain} in ${group}`,
96
96
-
// actions: [
97
97
-
// {'action': 'bsky', title: 'Bluesky'},
98
98
-
// {'action': 'spacedust', title: 'All notifications'},
99
99
-
// ],
62
62
+
body: `from @${handle}`,
100
63
});
101
64
102
65
ev.waitUntil(notification);