buttons for selecting notification sources

+119 -6
+49
atproto-notifications/src/components/Buttons.css
···
··· 1 + button.bg:focus-visible { 2 + outline: 4px auto -webkit-focus-ring-color; 3 + } 4 + 5 + button.bg { 6 + font: inherit; 7 + cursor: pointer; 8 + padding: 0 0.5rem; 9 + } 10 + button.bg:not(.bg-subtle) { 11 + background: #111; 12 + border: 1px solid hsla(0, 0%, 50%, 0.5); 13 + border-radius: 0.5rem; 14 + } 15 + 16 + button.bg.bg-subtle { 17 + background: transparent; 18 + border: 0; 19 + } 20 + button.bg.bg-subtle:hover { 21 + color: inherit; 22 + background: hsla(0, 0%, 0%, 0.3); 23 + } 24 + 25 + button.bg.big { 26 + padding: 0.25rem 0.75rem; 27 + } 28 + 29 + .button-group.vertical { 30 + display: flex; 31 + flex-direction: column; 32 + } 33 + 34 + .button-group > button.bg { 35 + color: #888; 36 + } 37 + .button-group > button.bg.current { 38 + font-weight: bold; 39 + color: #99bb77; 40 + } 41 + .button-group > button.bg.current.bg-subtle { 42 + background: #111; /*hsla(0, 0%, 0%, 0.2);*/ 43 + } 44 + 45 + @media screen and (max-width: 600px) { 46 + button.bg.bg-subtle { 47 + font-size: 0.85rem; 48 + } 49 + }
+17
atproto-notifications/src/components/Buttons.tsx
···
··· 1 + import './Buttons.css'; 2 + 3 + export function ButtonGroup({ options, current, onChange, subtle, big, vertical }) { 4 + return ( 5 + <div className={`button-group ${vertical ? 'vertical' : ''}`}> 6 + {options.map(({val, label}) => ( 7 + <button 8 + key={val} 9 + className={`bg ${subtle ? 'bg-subtle' : ''} ${big ? 'big' : ''} ${val === current ? 'current' : ''}`} 10 + onClick={() => onChange(val)} 11 + > 12 + {label ?? val} 13 + </button> 14 + ))} 15 + </div> 16 + ); 17 + }
+50 -5
atproto-notifications/src/components/Feed.tsx
··· 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 26 // for now, we just increment a counter when a new notif comes in, which forces a re-render 27 const [inc, setInc] = useState(0); ··· 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 ))}
··· 1 import { useEffect, useState } from 'react'; 2 import { getNotifications, getSecondary } from '../db'; 3 + import { ButtonGroup } from './Buttons'; 4 + import psl from 'psl'; 5 + import lexicons from 'lexicons'; 6 7 function Asdf({ inc, secondary }) { 8 const [secondaries, setSecondaries] = useState([]); 9 + const [selected, setSelected] = useState(null); 10 + 11 useEffect(() => { 12 (async () => { 13 const secondaries = await getSecondary(secondary); 14 secondaries.sort((a, b) => b.unread - a.unread); 15 setSecondaries(secondaries); 16 + // setSelected(secondaries[0]?.k); // TODO 17 })(); 18 }, [inc, secondary]); 19 20 return ( 21 <div> 22 + <ButtonGroup 23 + options={secondaries.map(({ k, unread, total }) => { 24 + 25 + 26 + let title = k; 27 + if (secondary === 'source') { 28 + // TODO: clean up / move this to lexicons package? 29 + let app; 30 + let appPrefix; 31 + try { 32 + const [nsid, ...rp] = k.split(':'); 33 + const parts = nsid.split('.'); 34 + const unreversed = parts.toReversed().join('.'); 35 + app = psl.parse(unreversed)?.domain ?? 'unknown'; 36 + appPrefix = app.split('.').toReversed().join('.'); 37 + } catch (e) { 38 + console.error('getting top app failed', e); 39 + } 40 + const lex = lexicons[appPrefix]; 41 + const icon = lex?.clients[0]?.icon; 42 + title = lex?.known_sources[k.slice(app.length + 1)] ?? k; 43 + } 44 + 45 + return { val: k, label: `${title} (${total})` }; 46 + })} 47 + current={selected} 48 + onChange={setSelected} 49 + subtle 50 + /> 51 + 52 + 53 </div> 54 ); 55 } 56 57 export function Feed() { 58 + const [secondary, setSecondary] = useState('all'); 59 60 // for now, we just increment a counter when a new notif comes in, which forces a re-render 61 const [inc, setInc] = useState(0); ··· 78 } 79 return ( 80 <div className="feed"> 81 + <ButtonGroup 82 + options={[ 83 + {val: 'all', label: 'All'}, 84 + {val: 'app', label: 'App'}, 85 + {val: 'group', label: 'Lexicon group'}, 86 + {val: 'source', label: 'Every source'}, 87 + ]} 88 + current={secondary} 89 + onChange={setSecondary} 90 + subtle 91 + /> 92 + <Asdf inc={inc} secondary={secondary} /> 93 {feed.map(([k, n]) => ( 94 <p key={k}>{k}: {n.source} ({n.source_record}) <code>{JSON.stringify(n)}</code></p> 95 ))}
+2 -1
atproto-notifications/src/db.ts
··· 1 const NOTIFICATIONS = 'notifications'; 2 - const SECONDARIES = ['all', 'source', 'group', 'app']; 3 4 export const getDB = ((upgrade, v) => { 5 let instance;
··· 1 const NOTIFICATIONS = 'notifications'; 2 + 3 + export const SECONDARIES = ['all', 'source', 'group', 'app']; 4 5 export const getDB = ((upgrade, v) => { 6 let instance;
+1
atproto-notifications/src/index.css
··· 61 margin: 0; 62 padding: 0; 63 font: inherit; 64 } 65 button.bad { 66 color: tomato;
··· 61 margin: 0; 62 padding: 0; 63 font: inherit; 64 + box-shadow: none; 65 } 66 button.bad { 67 color: tomato;