at main 126 lines 4.1 kB view raw
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}