+15
-1
atproto-notifications/package-lock.json
+15
-1
atproto-notifications/package-lock.json
···
15
15
"react": "^19.1.0",
16
16
"react-dom": "^19.1.0",
17
17
"react-router": "^7.6.3",
18
-
"react-time-ago": "^7.3.3"
18
+
"react-time-ago": "^7.3.3",
19
+
"reactjs-popup": "^2.0.6"
19
20
},
20
21
"devDependencies": {
21
22
"@eslint/js": "^9.29.0",
···
3115
3116
"javascript-time-ago": "^2.3.7",
3116
3117
"react": ">=0.16.8",
3117
3118
"react-dom": ">=0.16.8"
3119
+
}
3120
+
},
3121
+
"node_modules/reactjs-popup": {
3122
+
"version": "2.0.6",
3123
+
"resolved": "https://registry.npmjs.org/reactjs-popup/-/reactjs-popup-2.0.6.tgz",
3124
+
"integrity": "sha512-A+tt+x9wdgZiZjv0e2WzYLD3IfFwJALaRaqwrCSXGjo0iQdsry/EtBEbQXRSmQs7cHmOi5eytCiSlOm8k4C+dg==",
3125
+
"license": "MIT",
3126
+
"engines": {
3127
+
"node": ">=10"
3128
+
},
3129
+
"peerDependencies": {
3130
+
"react": ">=16",
3131
+
"react-dom": ">=16"
3118
3132
}
3119
3133
},
3120
3134
"node_modules/relative-time-format": {
+2
-1
atproto-notifications/package.json
+2
-1
atproto-notifications/package.json
atproto-notifications/public/icons/microcosm.png
atproto-notifications/public/icons/microcosm.png
This is a binary file and will not be displayed.
+22
atproto-notifications/src/components/NotificationSettings.tsx
+22
atproto-notifications/src/components/NotificationSettings.tsx
···
1
+
import { useState } from 'react';
2
+
3
+
1
4
export function NotificationSettings({ secondary, secondaryFilter }) {
5
+
6
+
7
+
// const [notifyToggleCounter, setNotifyToggleCounter] = useState(0);
8
+
9
+
// // TODO move up (to chrome?) so it syncs
10
+
// const setGlobalNotifications = useCallback(async enabled => {
11
+
// const host = import.meta.env.VITE_NOTIFICATIONS_HOST;
12
+
// const url = new URL('/global-notify', host);
13
+
// try {
14
+
// await postJson(url, JSON.stringify({ notify_enabled: enabled }), true)
15
+
// } catch (err) {
16
+
// console.error('failed to set self-notify setting', err);
17
+
// }
18
+
// setNotifyToggleCounter(n => n + 1);
19
+
// });
20
+
21
+
22
+
23
+
2
24
if (secondary === 'all') {
3
25
return <p>Notifications default: [todo: toggle mute], unknown sources: [toggle mute]</p>;
4
26
}
-3
atproto-notifications/src/pages/Early.tsx
-3
atproto-notifications/src/pages/Early.tsx
···
39
39
40
40
// TODO move up (to chrome?) so it syncs
41
41
const setGlobalNotifications = useCallback(async enabled => {
42
-
// setSecretDevStatus('pending');
43
42
const host = import.meta.env.VITE_NOTIFICATIONS_HOST;
44
43
const url = new URL('/global-notify', host);
45
44
try {
46
45
await postJson(url, JSON.stringify({ notify_enabled: enabled }), true)
47
-
// setSecretDevStatus(null);
48
46
} catch (err) {
49
47
console.error('failed to set self-notify setting', err);
50
-
// setSecretDevStatus('failed');
51
48
}
52
49
setNotifyToggleCounter(n => n + 1);
53
50
});
+40
atproto-notifications/src/pages/Feed.css
+40
atproto-notifications/src/pages/Feed.css
···
26
26
text-align: left;
27
27
margin: 2rem auto;
28
28
}
29
+
30
+
.filter-pref-trigger {
31
+
display: inline-block;
32
+
padding: 0 0.25rem;
33
+
}
34
+
.filter-pref-trigger:hover {
35
+
background: hsla(0, 0%, 50%, 0.333);
36
+
border-radius: 0.3333rem;
37
+
}
38
+
39
+
.popup-arrow {
40
+
color: #2c343c;
41
+
stroke-width: 1.5px;
42
+
stroke: hsla(0, 0%, 50%, 0.333);
43
+
stroke-dasharray: 30px;
44
+
stroke-dashoffset: -54px;
45
+
}
46
+
47
+
.popup-overlay {
48
+
background: hsla(0, 0%, 0%, 0.1);
49
+
}
50
+
51
+
.popup-content {
52
+
background: #2c343c;
53
+
padding: 0.25rem 0.333rem;
54
+
font-size: 0.8rem;
55
+
border: 0.5px solid hsla(0, 0%, 50%, 0.333);
56
+
border-radius: 0.25rem;
57
+
}
58
+
.filter-pref-popup {
59
+
text-align: center;
60
+
}
61
+
.filter-pref-popup h4 {
62
+
margin: 0 0 0.25rem;
63
+
font-size: 0.8rem;
64
+
color: #bbb;
65
+
}
66
+
.filter-pref.option {
67
+
display: block;
68
+
}
+40
-1
atproto-notifications/src/pages/Feed.tsx
+40
-1
atproto-notifications/src/pages/Feed.tsx
···
1
1
import { useEffect, useState } from 'react';
2
+
import Popup from 'reactjs-popup';
2
3
import { getNotifications, getSecondary } from '../db';
3
4
import { ButtonGroup } from '../components/Buttons';
4
5
import { NotificationSettings } from '../components/NotificationSettings';
5
6
import { Notification } from '../components/Notification';
7
+
import { GetJson } from '../components/Fetch';
6
8
import psl from 'psl';
7
9
import lexicons from 'lexicons';
8
10
9
11
import './feed.css';
12
+
13
+
function FilterPref({ secondary, value }) {
14
+
return (
15
+
<Popup
16
+
trigger={
17
+
<div className="filter-pref-trigger">
18
+
⚙
19
+
</div>
20
+
}
21
+
position={['bottom center']}
22
+
closeOnDocumentClick
23
+
>
24
+
<div className="filter-pref-popup">
25
+
<h4>filter notifications</h4>
26
+
<ButtonGroup
27
+
options={[
28
+
{ val: 'notify', label: 'notify' },
29
+
{ val: 'mute' },
30
+
]}
31
+
current={null}
32
+
/>
33
+
{/*<button className="subtle">reset</button>*/}
34
+
</div>
35
+
</Popup>
36
+
);
37
+
}
10
38
11
39
function SecondaryFilter({ inc, secondary, current, onUpdate }) {
12
40
const [secondaries, setSecondaries] = useState([]);
···
80
108
{icon && (
81
109
<img className="app-icon" src={icon} title={appName ?? app} alt="" />
82
110
)}
83
-
{title} ({total})
111
+
{title}
112
+
<small style={{
113
+
display: 'inline-block',
114
+
fontSize: '0.6rem',
115
+
padding: '0 0.2rem',
116
+
color: '#f90',
117
+
fontFamily: 'monospace',
118
+
verticalAlign: 'top',
119
+
}}>
120
+
{total >= 30 ? '30+' : total}
121
+
</small>
122
+
<FilterPref secondary={secondary} value={k} />
84
123
</>
85
124
),
86
125
};
+6
-1
lexicons/index.js
+6
-1
lexicons/index.js
···
2
2
'blue.microcosm': {
3
3
name: 'microcosm',
4
4
clients: [
5
-
{},
5
+
{
6
+
app_name: 'Spacedust notifications demo',
7
+
canonical: true,
8
+
main: 'https://notifications.microcosm.blue',
9
+
icon: '/icons/microcosm.png',
10
+
},
6
11
],
7
12
known_sources: {
8
13
'test.notification:hello': 'Hello spacedust!',
+13
server/schema.sql
+13
server/schema.sql
···
30
30
31
31
check(length(password) >= 3)
32
32
) strict;
33
+
34
+
create table if not exists mute_by_secondary (
35
+
account_did text not null,
36
+
selector text not null,
37
+
selection text not null,
38
+
mute integer not null default true,
39
+
40
+
primary key(account_did, selector, selection),
41
+
check(selector in ('all', 'app', 'group', 'source')),
42
+
43
+
foreign key(account_did) references accounts(did)
44
+
on delete cascade on update cascade
45
+
) strict;