+1
-1
atproto-notifications/index.html
+1
-1
atproto-notifications/index.html
+30
-6
atproto-notifications/src/components/Feed.tsx
+30
-6
atproto-notifications/src/components/Feed.tsx
···
1
import { useEffect, useState } from 'react';
2
-
import { getNotifications } from '../db';
3
4
export function Feed() {
5
···
16
// this could be combined with the broadcast thing above, but for now just chain deps
17
const [feed, setFeed] = useState([]);
18
useEffect(() => {
19
-
(async () => setFeed((await getNotifications())))();
20
}, [inc]);
21
22
if (feed.length === 0) {
23
return 'no notifications loaded';
24
}
25
-
return feed.map(([k, n]) => (
26
-
<p key={k}>{k}: {n.source} ({n.source_record}) <code>{JSON.stringify(n)}</code></p>
27
-
));
28
-
29
}
···
1
import { useEffect, useState } from 'react';
2
+
import { getNotifications, getSecondary } from '../db';
3
+
4
+
function Asdf({ inc, secondary }) {
5
+
const [secondaries, setSecondaries] = useState([]);
6
+
useEffect(() => {
7
+
(async () => {
8
+
const secondaries = await getSecondary(secondary);
9
+
secondaries.sort((a, b) => b.unread - a.unread);
10
+
setSecondaries(secondaries);
11
+
})();
12
+
}, [inc, secondary]);
13
+
14
+
return (
15
+
<div>
16
+
<p>secondaries: ({secondaries.length})</p>
17
+
{secondaries.map(a => (
18
+
<p key={a.k}>asdf {a.k} ({a.unread}/{a.total})</p>
19
+
))}
20
+
</div>
21
+
);
22
+
}
23
24
export function Feed() {
25
···
36
// this could be combined with the broadcast thing above, but for now just chain deps
37
const [feed, setFeed] = useState([]);
38
useEffect(() => {
39
+
(async () => setFeed(await getNotifications()))();
40
}, [inc]);
41
42
if (feed.length === 0) {
43
return 'no notifications loaded';
44
}
45
+
return (
46
+
<div className="feed">
47
+
<Asdf inc={inc} secondary='source' />
48
+
{feed.map(([k, n]) => (
49
+
<p key={k}>{k}: {n.source} ({n.source_record}) <code>{JSON.stringify(n)}</code></p>
50
+
))}
51
+
</div>
52
+
);
53
}
+22
-6
atproto-notifications/src/db.ts
+22
-6
atproto-notifications/src/db.ts
···
18
// primary store for notifications
19
try {
20
// upgrade is a reset: entirely remove the store (ignore errors if it didn't exist)
21
-
db.deleteObjectStore('notifs');
22
} catch (e) {}
23
const notifStore = db.createObjectStore(NOTIFICATIONS, {
24
-
key: 'id',
25
autoIncrement: true,
26
});
27
// subject prob doesn't need an index, could just query constellation
···
44
db.deleteObjectStore(secondary);
45
} catch (e) {}
46
const store = db.createObjectStore(secondary, {
47
-
key: 'k',
48
});
49
store.createIndex('total', 'total', { unique: false });
50
store.createIndex('unread', 'unread', { unique: false });
51
}
52
53
-
}, 3);
54
55
export async function insertNotification(notif: {
56
subject: String,
···
71
const store = tx.objectStore(secondary);
72
const key = secondary === 'all' ? 'all' : notif[secondary];
73
store.get(key).onsuccess = ev => {
74
-
let count = ev.target.result ?? { total: 0, unread: 0 };
75
count.total += 1;
76
count.unread += 1;
77
-
store.put(count, key);
78
};
79
}
80
···
104
}
105
});
106
}
···
18
// primary store for notifications
19
try {
20
// upgrade is a reset: entirely remove the store (ignore errors if it didn't exist)
21
+
db.deleteObjectStore(NOTIFICATIONS);
22
} catch (e) {}
23
const notifStore = db.createObjectStore(NOTIFICATIONS, {
24
+
keyPath: 'id',
25
autoIncrement: true,
26
});
27
// subject prob doesn't need an index, could just query constellation
···
44
db.deleteObjectStore(secondary);
45
} catch (e) {}
46
const store = db.createObjectStore(secondary, {
47
+
keyPath: 'k',
48
});
49
store.createIndex('total', 'total', { unique: false });
50
store.createIndex('unread', 'unread', { unique: false });
51
}
52
53
+
}, 4);
54
55
export async function insertNotification(notif: {
56
subject: String,
···
71
const store = tx.objectStore(secondary);
72
const key = secondary === 'all' ? 'all' : notif[secondary];
73
store.get(key).onsuccess = ev => {
74
+
let count = ev.target.result ?? {
75
+
k: key,
76
+
total: 0,
77
+
unread: 0,
78
+
};
79
count.total += 1;
80
count.unread += 1;
81
+
store.put(count);
82
};
83
}
84
···
108
}
109
});
110
}
111
+
112
+
export async function getSecondary(secondary) {
113
+
const db = await getDB();
114
+
const obj = db
115
+
.transaction([secondary])
116
+
.objectStore(secondary)
117
+
.getAll();
118
+
return new Promise((resolve, reject) => {
119
+
obj.onerror = () => reject(obj.error);
120
+
obj.onsuccess = ev => resolve(ev.target.result);
121
+
});
122
+
}
-1
atproto-notifications/src/service-worker.ts
-1
atproto-notifications/src/service-worker.ts
···
36
// TODO: user pref for alt client -> prefer that client's icon
37
const lex = lexicons[appPrefix];
38
const icon = lex?.clients[0]?.icon;
39
-
console.log('app', app, 'lex', lex, lexicons);
40
const title = lex?.known_sources[source.slice(app.length + 1)] ?? source;
41
const body = `from @${handle} on ${lex?.name ?? app}`;
42
+1
-1
atproto-notifications/vite.config.ts
+1
-1
atproto-notifications/vite.config.ts
+9
-1
server/index.js
+9
-1
server/index.js
···
25
if (!subs.has(did)) {
26
subs.set(did, []);
27
}
28
subs.get(did).push(sub);
29
updateSubs();
30
};
···
70
}
71
72
const expiredSubs = [];
73
-
for (const sub of subs.get(did) ?? []) { try {
74
await webpush.sendNotification(sub, JSON.stringify({ subject, source, source_record }));
75
} catch (err) {
76
if (400 <= err.statusCode && err.statusCode < 500) {
···
25
if (!subs.has(did)) {
26
subs.set(did, []);
27
}
28
+
sub.t = new Date();
29
subs.get(did).push(sub);
30
updateSubs();
31
};
···
71
}
72
73
const expiredSubs = [];
74
+
const now = new Date();
75
+
for (const sub of subs.get(did) ?? []) {
76
+
try {
77
+
if (now - sub.t < 1500) {
78
+
console.warn('skipping for rate limit');
79
+
continue;
80
+
}
81
+
sub.t = now;
82
await webpush.sendNotification(sub, JSON.stringify({ subject, source, source_record }));
83
} catch (err) {
84
if (400 <= err.statusCode && err.statusCode < 500) {