+13
atproto-notifications/src/components/Notification.css
+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
+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
+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
+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',