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, }); } }