diff options
| author | s-ol <s+removethis@s-ol.nu> | 2022-01-13 17:27:20 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2022-01-13 17:27:20 +0000 |
| commit | 0c98276d324db649c4a15be7859a501588257972 (patch) | |
| tree | 8e2ee4784730e16290b043ee4b38418fa5166106 /client/src/graph.js | |
| parent | Menu, DiscDAG loading (diff) | |
| download | fedidag-0c98276d324db649c4a15be7859a501588257972.tar.gz fedidag-0c98276d324db649c4a15be7859a501588257972.zip | |
move everything into client/
Diffstat (limited to 'client/src/graph.js')
| -rw-r--r-- | client/src/graph.js | 205 |
1 files changed, 205 insertions, 0 deletions
diff --git a/client/src/graph.js b/client/src/graph.js new file mode 100644 index 0000000..7fe842c --- /dev/null +++ b/client/src/graph.js @@ -0,0 +1,205 @@ +import { Component } from 'preact'; +import * as jsonld from 'jsonld'; +import { CORS_PREFIX } from './config'; + +const cors = (url) => `${CORS_PREFIX}/${url}`; + +function wrapCache(target) { + const orig = target.descriptor.value; + + target.descriptor.value = function (id, ...args) { + this.cache[id] ||= orig.call(this, id, ...args); + return this.cache[id]; + }; + + return target; +}; + +const context = [ + 'https://www.w3.org/ns/activitystreams', + { + replies: { '@id': 'as:replies', '@container': '@set' }, + inReplyTo: { '@id': 'as:inReplyTo', '@container': '@set' }, + items: { '@id': 'as:items', '@type': '@id', '@container': '@set' }, + }, +]; + +export class GraphContainer extends Component { + state = { + loading: true, + name: "loading…", + items: {}, + error: null, + }; + + cache = {}; + + componentDidMount() { + this.componentDidUpdate({}); + } + + componentDidUpdate(prevProps) { + if (prevProps.url === this.props.url) return; + + this.loadData(this.props.url) + .catch(error => { + console.error(`Error loading ${this.props.url}:`, error); + this.setState({ + loading: false, + error, + }); + }); + } + + @wrapCache + async loadUser(id) { + 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.state = this.state; + return this.props.render(this.state); + } +} + +export class GraphContainerMastodon extends GraphContainer { + @wrapCache + async loadCollection(id, ...args) { + const collection = await jsonld.compact(cors(id), context); + let items = []; + + const addItems = (i) => { + if (Array.isArray(i)) + items = items.concat(i); + else if (i) + items.push(i); + }; + + // discover items + if (collection.items) { + addItems(collection.items); + } else { + let page = collection.first; + const seen = []; + while (page && seen.indexOf(page.id) < 0) { + seen.push(page.id); + addItems(page.items); + + page = page.next && await jsonld.compact(cors(page.next), context); + } + } + + // dereference items + return await Promise.all(items.map(item => this.loadNote(item.id ?? item, ...args))); + } + + @wrapCache + 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, + 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); + items[item.id] = item; + return item; + } + + async loadData(url) { + const items = {}; + const root = await this.loadNote(url, items, []); + + this.setState({ + name: root.name || root.content, + items, + loading: false, + }); + } +} + +export class GraphContainerJSONLD extends GraphContainer { + async loadData(url) { + const response = await fetch(url, { + headers: { 'Accept': 'application/ld+json, application/json' }, + credentials: 'include', + }); + + const discussion = await jsonld.frame( + await response.json(), + { + '@context': context, + type: 'Document', + first: { '@embed': '@never' }, + items: { + context: { '@embed': '@never' }, + replies: { '@embed': '@never' }, + inReplyTo: { '@embed': '@never' }, + attributedTo: { '@embed': '@always' }, + }, + }, + { omitGraph: true } + ); + + const indexedItems = {}; + await Promise.all(discussion.items.map(async (item) => { + item.attributedTo = await this.loadUser(item.attributedTo); + indexedItems[item.id] = item; + })); + + discussion.items = indexedItems; + + this.setState({ + ...discussion, + loading: false, + }); + } +} |
