demos for spacedust
1import { useState, useEffect } from 'react';
2import ReactTimeAgo from 'react-time-ago';
3import psl from 'psl';
4import { default as lexicons, getLink, getContext } from 'lexicons';
5import { resolveDid } from '../atproto/resolve';
6import { Fetch } from './Fetch';
7
8import './Notification.css';
9
10export function fallbackRender({ error, resetErrorBoundary }) {
11 console.error('rendering fallback for error', error);
12 return (
13 <div className="notification error">
14 <p>sorry, something went wrong trying to show this notification</p>
15 <p><button onClick={resetErrorBoundary}>retry</button></p>
16 </div>
17 );
18}
19
20export function Notification({ app, group, source, source_record, source_did, subject, timestamp }) {
21 const [resolvedLink, setResolvedLink] = useState(null);
22 const [resolvedContext, setResolvedContext] = useState([]);
23
24 useEffect(() => {
25 (async () => {
26 const link = await getLink(source, source_record, subject);
27 if (link) setResolvedLink(link);
28 })();
29 (async() => {
30 const context = await getContext(source, source_record, subject);
31 setResolvedContext(context);
32 })();
33 }, [source, source_record, subject]);
34
35 // TODO: clean up / move this to lexicons package?
36 let title = source;
37 let icon;
38 let appName;
39 let appPrefix;
40 try {
41 appPrefix = app.split('.').toReversed().join('.');
42 } catch (e) {
43 console.error('getting top app failed', e);
44 }
45 const lex = lexicons[appPrefix];
46 icon = lex?.clients[0]?.icon;
47 let link = lex?.clients[0]?.notifications;
48 appName = lex?.name;
49 const sourceRemainder = source.slice(app.length + 1);
50 title = lex?.known_sources[sourceRemainder]?.name ?? source;
51
52 let directLink;
53 if (subject.startsWith('did:')) {
54
55 const s = source_record.slice('at://'.length).split('/');
56 const [sDid, sCollection, sRest] = [s[0], s[1], s.slice(2)]; // yeah did might be a handle oh well
57
58 directLink = lex
59 ?.clients[0]
60 ?.direct_links?.[`did:${sourceRemainder}`]
61 ?.replace('{subject.did}', subject)
62 ?.replace('{source_record.did}', sDid)
63 ?.replace('{source_record.collection}', sCollection)
64 ?.replace('{source_record.rkey}', sRest.join('/') || null);
65
66 } else if (subject.startsWith('at://')) {
67 let s = subject.slice('at://'.length).split('/');
68 const [did, collection, rest] = [s[0], s[1], s.slice(2)]; // yeah did might be a handle oh well
69
70 s = source_record.slice('at://'.length).split('/');
71 const [sDid, sCollection, sRest] = [s[0], s[1], s.slice(2)]; // yeah did might be a handle oh well
72
73 directLink = lex
74 ?.clients[0]
75 ?.direct_links?.[`at_uri:${sourceRemainder}`]
76 ?.replace('{subject.did}', did)
77 ?.replace('{subject.collection}', collection)
78 ?.replace('{subject.rkey}', rest.join('/') || null)
79 ?.replace('{source_record.did}', sDid)
80 ?.replace('{source_record.collection}', sCollection)
81 ?.replace('{source_record.rkey}', sRest.join('/') || null);
82 }
83 link = resolvedLink ?? directLink ?? link;
84
85 let contextClipped = resolvedContext.join(' ');
86 if (contextClipped.length > 240) {
87 contextClipped = contextClipped.slice(0, 239) + '…';
88 }
89
90 const contents = (
91 <>
92 <div className="notification-info">
93 {icon && (
94 <img className="app-icon" src={icon} title={appName ?? app} alt="" />
95 )}
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>
112 </div>
113 {timestamp && (
114 <div className="notification-when">
115 <ReactTimeAgo date={new Date(timestamp)} locale="en-US"/>
116 </div>
117 )}
118 </>
119 );
120
121 return link
122 ? <a className="notification" href={link} target="_blank">
123 {contents}
124 </a>
125 : <div className="notification">{contents}</div>;
126}