aboutsummaryrefslogtreecommitdiffstats
path: root/client/src/graph.js
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2022-01-13 17:27:20 +0000
committers-ol <s+removethis@s-ol.nu>2022-01-13 17:27:20 +0000
commit0c98276d324db649c4a15be7859a501588257972 (patch)
tree8e2ee4784730e16290b043ee4b38418fa5166106 /client/src/graph.js
parentMenu, DiscDAG loading (diff)
downloadfedidag-0c98276d324db649c4a15be7859a501588257972.tar.gz
fedidag-0c98276d324db649c4a15be7859a501588257972.zip
move everything into client/
Diffstat (limited to 'client/src/graph.js')
-rw-r--r--client/src/graph.js205
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,
+ });
+ }
+}