diff options
| -rw-r--r-- | src/index.js | 156 | ||||
| -rw-r--r-- | src/ui/Note.js | 14 |
2 files changed, 122 insertions, 48 deletions
diff --git a/src/index.js b/src/index.js index 38dc8cc..5fdda36 100644 --- a/src/index.js +++ b/src/index.js @@ -33,6 +33,7 @@ class GraphContainer extends Component { state = { name: "loading…", items: {}, + error: null, }; cache = {}; @@ -42,10 +43,22 @@ class GraphContainer extends Component { if (this.props.graph.startsWith('http')) this.loadDataMastodon(this.props.graph) - .catch(err => console.error(err)); + .catch(error => { + console.error(`Error loading Mastodon graph ${this.props.graph}:`, error); + this.setState({ + name: "error loading", + error, + }); + }); else this.loadData(this.props.graph) - .catch(err => console.error(err)); + .catch(error => { + console.error("Error loading default graph:", error); + this.setState({ + name: "error loading", + error, + }); + }); } @wrapCache @@ -79,29 +92,46 @@ class GraphContainer extends Component { } @wrapCache - async loadNote(id, items) { - const item = await jsonld.frame( - cors(id), - { + async loadNote(id, items, inReplyTo) { + let item; + try { + item = await jsonld.frame( + cors(id), + { + '@context': context, + type: 'Note', + context: { '@embed': '@never' }, + replies: { '@embed': '@always' }, + inReplyTo: { '@embed': '@never' }, + }, + { omitGraph: true } + ); + + if (item.replies.length) + item.replies = await this.loadCollection(item.replies[0].id, items, [id]); + } catch (error) { + console.error(`Error loading note '${id}':`, error); + item = { '@context': context, - type: 'Note', - context: { '@embed': '@never' }, - replies: { '@embed': '@always' }, - inReplyTo: { '@embed': '@never' }, - }, - { omitGraph: true } - ); + id, + type: 'Tombstone', + formerType: 'Note', + attributedTo: 'https://dag.s-ol.nu/users/unknown', + published: '1970-01-01T00:00:00Z', + content: `Error loading note: ${error.toString()}`, + replies: [], + inReplyTo, + }; + } item.attributedTo = await this.loadUser(item.attributedTo); - item.replies = await this.loadCollection(item.replies[0].id, items); - items[item.id] = item; return item; } async loadDataMastodon(url) { const items = {}; - const root = await this.loadNote(url, items); + const root = await this.loadNote(url, items, []); this.setState({ name: root.name || root.content, @@ -111,9 +141,7 @@ class GraphContainer extends Component { async loadData(url) { const response = await fetch(url, { - headers: { - 'Accept': 'application/json', - }, + headers: { 'Accept': 'application/json' }, }); const discussion = await jsonld.frame( await response.json(), @@ -143,13 +171,34 @@ class GraphContainer extends Component { @wrapCache async loadUser(id) { - const user = await jsonld.compact(id, context); - user.id = id; - return user; + if (id === "https://dag.s-ol.nu/users/unknown") { + return { + '@context': context, + id, + type: 'Person', + name: 'unknown', + summary: 'unknown user', + }; + } + + try { + const user = await jsonld.compact(id, context); + user.id = id; + return user; + } catch (error) { + console.error(`Error loading user '${id}':`, error); + return { + '@context': context, + id, + type: 'Person', + name: id, + summary: 'failed to load user information', + }; + } } render() { - window.s = this.state; + window.state = this.state; return this.props.render(this.state); } } @@ -213,28 +262,43 @@ class CollapseContainer extends Component { const search = new URLSearchParams(window.location.search); const graph = search.has('graph') ? search.get('graph') : 'lib/graph.json'; const app = ( - <GraphContainer graph={graph} render={({ name, items }) => ( - <SelectionContainer render={({ selection, toggleSelected }) => ( - <> - <CollapseContainer - items={items} - render={({ collapsed, toggleCollapsed }) => ( - <Discussion - name={name} - items={items} - collapsed={collapsed} - toggleCollapsed={toggleCollapsed} - toggleSelected={toggleSelected} - /> - )} - /> - <Selection - items={selection.map(id => items[id])} - toggleSelected={toggleSelected} - /> - </> - )} /> - )} /> + <GraphContainer graph={graph} render={({ name, items, error }) => { + if (error) { + return ( + <article> + <h1>{name}</h1> + <p>{error.toString()}</p> + <pre> + <code> + {error.stack} + </code> + </pre> + </article> + ); + } + + return ( + <SelectionContainer render={({ selection, toggleSelected }) => ( + <> + <CollapseContainer + items={items} + render={({ collapsed, toggleCollapsed }) => ( + <Discussion + name={name} + items={items} + collapsed={collapsed} + toggleCollapsed={toggleCollapsed} + toggleSelected={toggleSelected} + /> + )} + /> + <Selection + items={selection.map(id => items[id])} + toggleSelected={toggleSelected} + /> + </> + )} /> + ); + }} /> ); render(app, document.body); -window.app = app; diff --git a/src/ui/Note.js b/src/ui/Note.js index d4ef30a..63905e4 100644 --- a/src/ui/Note.js +++ b/src/ui/Note.js @@ -49,6 +49,14 @@ section > header > .avatar { flex: 0 0 auto; margin: 0.25rem; border-radius: 0.3rem; + + font-size: 1.5rem; + font-weight: bold; + line-height: 2rem; + text-align: center; + text-decoration: none; + background: white; + color: black; } section > header > .avatar img { width: inherit; @@ -78,7 +86,7 @@ section > header > .user { } section > header > .time { - flex: 0 1 auto; + flex: 0 0 auto; font-size: 0.8em; color: #363636; } @@ -94,7 +102,9 @@ const Header = ({ id, user, published, collapsed, onCollapse }) => { return ( <header> <a class="avatar" href={user.id}> - <img src={new URL(user.icon.url, user.id)} /> + {user.icon + ? <img src={new URL(user.icon.url, user.id)} /> + : "?"} </a> <span class="user">{username}</span> <Space /> |
