load records for notification context

Changed files
+71 -16
atproto-notifications
lexicons
+13
atproto-notifications/src/components/Notification.css
··· 7 7 box-sizing: border-box; 8 8 display: flex; 9 9 justify-content: space-between; 10 + gap: 0.5rem; 10 11 } 11 12 a.notification { 12 13 font: inherit; ··· 30 31 margin: 0; 31 32 } 32 33 34 + .notification-info { 35 + display: flex; 36 + align-items: baseline; 37 + } 38 + 33 39 .handle { 34 40 color: skyblue; 41 + } 42 + 43 + .notification-context { 44 + font-size: 0.8rem; 45 + opacity: 0.667; 46 + margin: 0.25rem 0 0; 47 + max-width: 48em; 35 48 } 36 49 37 50 .notification-when {
+27 -12
atproto-notifications/src/components/Notification.tsx
··· 1 1 import { useState, useEffect } from 'react'; 2 2 import ReactTimeAgo from 'react-time-ago'; 3 3 import psl from 'psl'; 4 - import { default as lexicons, getLink } from 'lexicons'; 4 + import { default as lexicons, getLink, getContext } from 'lexicons'; 5 5 import { resolveDid } from '../atproto/resolve'; 6 6 import { Fetch } from './Fetch'; 7 7 ··· 19 19 20 20 export function Notification({ app, group, source, source_record, source_did, subject, timestamp }) { 21 21 const [resolvedLink, setResolvedLink] = useState(null); 22 + const [resolvedContext, setResolvedContext] = useState([]); 22 23 23 24 useEffect(() => { 24 25 (async () => { 25 26 const link = await getLink(source, source_record, subject); 26 27 if (link) setResolvedLink(link); 28 + })(); 29 + (async() => { 30 + const context = await getContext(source, source_record, subject); 31 + setResolvedContext(context); 27 32 })(); 28 33 }, [source, source_record, subject]); 29 34 ··· 77 82 } 78 83 link = resolvedLink ?? directLink ?? link; 79 84 85 + let contextClipped = resolvedContext.join(' '); 86 + if (contextClipped.length > 240) { 87 + contextClipped = contextClipped.slice(0, 239) + '…'; 88 + } 89 + 80 90 const contents = ( 81 91 <> 82 92 <div className="notification-info"> 83 93 {icon && ( 84 94 <img className="app-icon" src={icon} title={appName ?? app} alt="" /> 85 95 )} 86 - {title} from 87 - {' '} 88 - {source_did ? ( 89 - <Fetch 90 - using={resolveDid} 91 - args={[source_did]} 92 - ok={handle => <span className="handle">@{handle}</span>} 93 - /> 94 - ) : ( 95 - source_record 96 - )} 96 + <div> 97 + {title} from 98 + {' '} 99 + {source_did ? ( 100 + <Fetch 101 + using={resolveDid} 102 + args={[source_did]} 103 + ok={handle => <span className="handle">@{handle}</span>} 104 + /> 105 + ) : ( 106 + source_record 107 + )} 108 + {contextClipped.length > 0 && ( 109 + <p className="notification-context">{contextClipped}</p> 110 + )} 111 + </div> 97 112 </div> 98 113 {timestamp && ( 99 114 <div className="notification-when">
+28 -4
lexicons/bits.js
··· 19 19 return [appSource, defs[appPrefix]]; 20 20 } 21 21 22 - export async function getContext(source, source_record, subject) { 23 - } 24 - 25 22 const uriBits = async uri => { 26 23 const bits = uri.slice('at://'.length).split('/'); 27 24 // TODO: identifier might be a handle ··· 70 67 const sub = JSONPath({ 71 68 path: `$.${match.groups.path}`, 72 69 json: subjectRecord, 73 - })[0]; 70 + })[0]; // TODO: array result? 74 71 75 72 link = link.replaceAll(match[0], sub); 76 73 } ··· 80 77 // 2.b TODO: source record lookups if needed 81 78 return link; 82 79 } 80 + 81 + export async function getContext(source, source_record, subject) { 82 + const [appSource, appDefs] = getAppDefs(source); 83 + const contexts = appDefs?.known_sources?.[appSource]?.context ?? []; 84 + const linkType = subject.startsWith('did:') ? 'did' : 'at_uri'; 85 + 86 + let loaded = []; 87 + for (const ctx of contexts) { 88 + const [o, ...pathstuff] = ctx.split(':'); 89 + if (o !== '@subject') { 90 + throw new Error('only @subject is implemented for context loading so far'); 91 + } 92 + if (linkType !== 'at_uri') { 93 + throw new Error('only at_uris can be used for @subject loading so far'); 94 + } 95 + const path = pathstuff.join(':'); 96 + const subjectRecord = await getAtUri(subject); 97 + // using json path is temporary -- need recordpath convention defined 98 + const found = JSONPath({ 99 + path, 100 + json: subjectRecord, 101 + }); 102 + loaded = loaded.concat(found); // TODO: think about array handling 103 + } 104 + 105 + return loaded; 106 + }
+3
lexicons/defs.js
··· 55 55 }, 56 56 'feed.like:subject.uri': { 57 57 name: 'Like', 58 + context: ['@subject:text'], 58 59 }, 59 60 'feed.like:via.uri': { 60 61 name: 'Repost like', ··· 118 119 main: 'https://tangled.sh', 119 120 direct_links: { 120 121 'at_uri:feed.star:subject': 'https://tangled.sh/{subject.did}/{@subject:name}', 122 + 'did:graph.follow:subject': 'https://tangled.sh/{source_record.did}', 121 123 }, 122 124 } 123 125 ], 124 126 known_sources: { 125 127 'feed.star:subject': { 126 128 name: 'Star', 129 + context: ['@subject:name'], 127 130 }, 128 131 'feed.reaction:subject': { 129 132 name: 'Reaction',