+8
-3
atproto-notifications/src/components/Buttons.css
+8
-3
atproto-notifications/src/components/Buttons.css
···
1
-
button {
1
+
button,
2
+
a.button {
2
3
border-radius: 0.5rem;
3
4
border: 1px solid transparent;
5
+
color: inherit;
4
6
padding: 0.6em 1.2em;
5
7
font-size: 1em;
6
8
font-weight: 500;
···
12
14
border-right-color: hsla(0, 0%, 0%, 0.3);
13
15
box-shadow: 0 42px 42px -42px inset #221828;
14
16
}
15
-
button:hover {
17
+
button:hover,
18
+
a.button:hover {
16
19
border-color: #646cff;
17
20
}
18
21
button:focus,
19
-
button:focus-visible {
22
+
button:focus-visible,
23
+
a.button:focus,
24
+
a.button:focus-visible {
20
25
outline: 4px auto -webkit-focus-ring-color;
21
26
}
22
27
+1
-1
atproto-notifications/src/components/Fetch.tsx
+1
-1
atproto-notifications/src/components/Fetch.tsx
+6
-5
atproto-notifications/src/components/SecretPassword.jsx
+6
-5
atproto-notifications/src/components/SecretPassword.jsx
···
38
38
}}
39
39
ok={() => (
40
40
<>
41
-
<p>That will do.</p>
41
+
<p style={{ color: "#9f0" }}>Secret password accepted.</p>
42
42
<p>
43
-
<button onClick={() => window.location.reload()}>
44
-
Enter
45
-
</button>
43
+
{/* an <a> tag, not a <Link>, on purpose so we relaod for our role */}
44
+
<a className="button" href="/early">
45
+
Continue
46
+
</a>
46
47
</p>
47
48
</>
48
49
)}
···
62
63
{' '}
63
64
{begun && (
64
65
<button type="submit" className="subtle">
65
-
open sesame
66
+
unlock
66
67
</button>
67
68
)}
68
69
</p>
+1
-1
atproto-notifications/src/components/setup/WithNotificationPermission.tsx
+1
-1
atproto-notifications/src/components/setup/WithNotificationPermission.tsx
+2
atproto-notifications/src/main.tsx
+2
atproto-notifications/src/main.tsx
···
5
5
import { App } from './App';
6
6
import { Feed } from './pages/Feed';
7
7
import { Admin } from './pages/Admin';
8
+
import { Early } from './pages/Early';
8
9
9
10
createRoot(document.getElementById('root')!).render(
10
11
// <StrictMode>
···
13
14
<Routes>
14
15
<Route index element={<Feed />} />
15
16
<Route path="/admin" element={<Admin />} />
17
+
<Route path="/early" element={<Early />} />
16
18
</Routes>
17
19
</App>
18
20
</BrowserRouter>
+5
atproto-notifications/src/pages/Early.css
+5
atproto-notifications/src/pages/Early.css
+57
atproto-notifications/src/pages/Early.tsx
+57
atproto-notifications/src/pages/Early.tsx
···
1
+
import { useCallback, useState } from 'react';
2
+
import { postJson } from '../components/Fetch';
3
+
import './Early.css';
4
+
5
+
export function Early({ }) {
6
+
const [pushCount, setPushCount] = useState(0);
7
+
const [pushStatus, setPushStatus] = useState(null);
8
+
9
+
const localTest = useCallback(() => {
10
+
new Notification("Hello world!", { body: "This notification never left your browser" });
11
+
});
12
+
13
+
const pushTest = useCallback(async () => {
14
+
setPushStatus(n => n + 1);
15
+
setPushStatus('pending');
16
+
const host = import.meta.env.VITE_NOTIFICATIONS_HOST;
17
+
const url = new URL('/push-test', host);
18
+
try {
19
+
await postJson(url, JSON.stringify(null), true);
20
+
setPushStatus(null);
21
+
} catch (e) {
22
+
console.error('failed push test request', e);
23
+
setPushStatus('failed');
24
+
}
25
+
});
26
+
27
+
return (
28
+
<div className="early">
29
+
<h2>Hello!</h2>
30
+
<p>Welcome to the early preview for the spacedust notifications demo, and since you're here early: thanks so much for supporting microcosm!</p>
31
+
<p>A few things to keep in mind:</p>
32
+
<ol>
33
+
<li>This is a demo, not a polished product</li>
34
+
<li>It has a lot of moving pieces, so things not always work</li>
35
+
<li>Many features can easily be added! Some others can't! Make a request and let's see :)</li>
36
+
<li>It's not a long-term committed part of microcosm <em>(yet)</em></li>
37
+
</ol>
38
+
<p>Sadly, it doesn't really work on mobile. On iOS you can hit "share" and "add to home screen" to get things eventually mostly set up, but push delivery will stop after a few minutes. Android people might have better luck?</p>
39
+
<h3>Hello hello</h3>
40
+
<p>With that out of the way, let's cover some basics.</p>
41
+
<p>
42
+
To see a test notification, <button onClick={localTest}>click on this</button>. This is a local-only test.
43
+
</p>
44
+
<p>
45
+
<button
46
+
disabled={pushStatus === 'pending'}
47
+
onClick={pushTest}
48
+
>
49
+
Click here {pushCount > 0 && <>({pushCount})</>}
50
+
</button>
51
+
{' '}
52
+
to see another. This one goes over Web Push.
53
+
</p>
54
+
{pushStatus === 'failed' && <p>uh oh, something went wrong requesting a web push</p>}
55
+
</div>
56
+
);
57
+
}
+9
lexicons/index.js
+9
lexicons/index.js
+17
-3
server/api.js
+17
-3
server/api.js
···
133
133
return gotIt(res);
134
134
};
135
135
136
+
const handlePushTest = async (db, user, res, push) => {
137
+
const subscription = db.getSubBySession(user.session);
138
+
const payload = JSON.stringify({
139
+
subject: user.did,
140
+
source: 'blue.microcosm.test.notification:hello',
141
+
source_record: `at://${user.did}/blue.microcosm.test.notification/test`,
142
+
timestamp: +new Date(),
143
+
});
144
+
await push(db, subscription, payload);
145
+
return okBye(res);
146
+
};
147
+
136
148
const handleLogout = async (db, user, req, res, appSecret, updateSubs) => {
137
149
try {
138
150
db.deleteSub(user.session);
···
152
164
const { secret_password } = JSON.parse(body);
153
165
const { did } = user;
154
166
const role = 'early';
155
-
console.log('going with', {did, role, secret_password});
156
167
const updated = db.setRole({ did, role, secret_password });
157
-
console.log('updated?', updated);
158
168
if (updated) {
159
169
return okBye(res);
160
170
} else {
···
226
236
}
227
237
}
228
238
229
-
export const server = (secrets, jwks, allowedOrigin, whoamiHost, db, updateSubs, adminDid) => {
239
+
export const server = (secrets, jwks, allowedOrigin, whoamiHost, db, updateSubs, push, adminDid) => {
230
240
const handler = (req, res) => {
231
241
// don't love this but whatever
232
242
const { pathname, searchParams } = new URL(`http://localhost${req.url}`);
···
260
270
if (method === 'POST' && pathname === '/subscribe') {
261
271
if (!user || user.role === 'public') return forbidden(res);
262
272
return handleSubscribe(db, user, req, res, updateSubs);
273
+
}
274
+
if (method === 'POST' && pathname === '/push-test') {
275
+
if (!user || user.role === 'public') return forbidden(res);
276
+
return handlePushTest(db, user, res, push);
263
277
}
264
278
265
279
// admin required (just 404 for non-admin)
+13
server/db.js
+13
server/db.js
···
11
11
#stmt_insert_push_sub;
12
12
#stmt_get_all_sub_dids;
13
13
#stmt_get_push_subs;
14
+
#stmt_get_push_sub;
14
15
#stmt_update_push_sub;
15
16
#stmt_delete_push_sub;
16
17
#stmt_get_push_info;
···
76
77
as 'since_last_push'
77
78
from push_subs
78
79
where account_did = ?`);
80
+
81
+
this.#stmt_get_push_sub = db.prepare(
82
+
`select session,
83
+
subscription,
84
+
(julianday(CURRENT_TIMESTAMP) - julianday(last_push)) * 24 * 60 * 60
85
+
as 'since_last_push'
86
+
from push_subs
87
+
where session = ?`);
79
88
80
89
this.#stmt_update_push_sub = db.prepare(
81
90
`update push_subs
···
157
166
158
167
getSubsByDid(did) {
159
168
return this.#stmt_get_push_subs.all(did);
169
+
}
170
+
171
+
getSubBySession(session) {
172
+
return this.#stmt_get_push_sub.get(session);
160
173
}
161
174
162
175
updateLastPush(session) {
+2
-2
server/index.js
+2
-2
server/index.js
···
47
47
const db = new DB(dbFilename, initDb);
48
48
49
49
const spacedustHost = env.SPACEDUST_HOST ?? 'wss://spacedust.microcosm.blue';
50
-
const updateSubs = connectSpacedust(db, spacedustHost);
50
+
const { updateSubs, push } = connectSpacedust(db, spacedustHost);
51
51
52
52
const host = env.HOST ?? 'localhost';
53
53
const port = parseInt(env.PORT ?? 8000, 10);
54
54
55
55
const allowedOrigin = env.ALLOWED_ORIGIN ?? 'http://127.0.0.1:5173';
56
56
57
-
server(secrets, jwks, allowedOrigin, whoamiHost, db, updateSubs, adminDid).listen(
57
+
server(secrets, jwks, allowedOrigin, whoamiHost, db, updateSubs, push, adminDid).listen(
58
58
port,
59
59
host,
60
60
() => console.log(`listening at http://${host}:${port} with allowed origin: ${allowedOrigin}`),