import {LitElement, html, css} from './lit-all.min.js'; import * as tfrpc from '/static/tfrpc.js'; import {styles} from './tf-styles.js'; class TfElement extends LitElement { static get properties() { return { whoami: {type: String}, ids: {type: Array}, messages: {type: Array}, users: {type: Object}, allFollowing: {type: Array}, status: {type: Array}, hash: {type: String}, unread: {type: Array}, tab: {type: String}, broadcasts: {type: Array}, connections: {type: Array}, }; } static styles = styles; constructor() { super(); let self = this; this.ids = []; this.users = {}; this.messages = []; this.allFollowing = []; this.status = []; this.messages_by_id = {}; this.hash = '#'; this.loading = false; this.unread = []; this.tab = 'news'; this.broadcasts = []; this.connections = []; tfrpc.rpc.getIdentities().then(ids => { self.ids = ids || [] }); tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || [] }); tfrpc.rpc.getConnections().then(c => { self.connections = c || [] }); tfrpc.rpc.getHash().then(hash => self.hash = hash || '#'); tfrpc.register(function hashChanged(hash) { self.hash = hash; self.load(); }); tfrpc.register(async function notifyNewMessage(id) { await self.fetch_new_message(id); }); tfrpc.register(function set(name, value) { if (name === 'broadcasts') { self.broadcasts = value; } else if (name === 'connections') { self.connections = value; } }); } async contacts_internal(id, last_row_id, following, max_row_id) { let result = Object.assign({}, following[id] || {}); result.following = result.following || {}; result.blocking = result.blocking || {}; let contacts = await tfrpc.rpc.query( ` SELECT content FROM messages WHERE author = ? AND rowid > ? AND rowid <= ? AND json_extract(content, "$.type") = "contact" ORDER BY sequence `, [id, last_row_id, max_row_id]); for (let row of contacts) { let contact = JSON.parse(row.content); if (contact.following === true) { result.following[contact.contact] = true; } else if (contact.following === false) { delete result.following[contact.contact]; } else if (contact.blocking === true) { result.blocking[contact.contact] = true; } else if (contact.blocking === false) { delete result.blocking[contact.contact]; } } return result; } async contact(id, last_row_id, following, max_row_id) { if (this.users[id]?.following) { return this.users[id]; } let result = await this.contacts_internal(id, last_row_id, following, max_row_id); let users = this.users; users[id] = Object.assign(users[id] || {}, result); following[id] = users[id]; this.users = users; return result; } async following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) { let contacts = await Promise.all([...new Set(ids)].map(x => this.contact(x, last_row_id, following, max_row_id))); let result = {}; for (let i = 0; i < ids.length; i++) { let id = ids[i]; let contact = contacts[i]; let found = Object.keys(contact.following).filter(y => !contact.blocking[y]); let deeper = depth > 1 ? await this.following_deep_internal(found, depth - 1, Object.assign({}, contact.blocking, blocking), last_row_id, following, max_row_id) : []; result[id] = [id, ...found, ...deeper]; } return [...new Set(Object.values(result).flat())]; } async following_deep(ids, depth, blocking) { const k_cache_version = 4; let cache = await tfrpc.rpc.databaseGet('following'); cache = cache ? JSON.parse(cache) : {}; if (cache.version !== k_cache_version) { cache = { version: k_cache_version, following: {}, last_row_id: 0, }; } let max_row_id = (await tfrpc.rpc.query(` SELECT MAX(rowid) AS max_row_id FROM messages `, []))[0].max_row_id; let result = await this.following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id); cache.last_row_id = max_row_id; await tfrpc.rpc.databaseSet('following', JSON.stringify(cache)); return result; } async fetch_about(ids) { const k_cache_version = 1; let cache = await tfrpc.rpc.databaseGet('about'); cache = cache ? JSON.parse(cache) : {}; if (cache.version !== k_cache_version) { cache = { version: k_cache_version, about: {}, last_row_id: 0, }; } let max_row_id = (await tfrpc.rpc.query(` SELECT MAX(rowid) AS max_row_id FROM messages `, []))[0].max_row_id; for (let id of Object.keys(cache.about)) { if (ids.indexOf(id) == -1) { delete cache.about[id]; } } let abouts = await tfrpc.rpc.query( ` SELECT messages.* FROM messages, json_each(?1) AS following WHERE messages.author = following.value AND messages.rowid > ?3 AND messages.rowid <= ?4 AND json_extract(messages.content, '$.type') = 'about' UNION SELECT messages.* FROM messages, json_each(?2) AS following WHERE messages.author = following.value AND messages.rowid <= ?4 AND json_extract(messages.content, '$.type') = 'about' ORDER BY messages.author, messages.sequence `, [ JSON.stringify(ids.filter(id => cache.about[id])), JSON.stringify(ids.filter(id => !cache.about[id])), cache.last_row_id, max_row_id, ]); for (let about of abouts) { let content = JSON.parse(about.content); if (content.about === about.author) { delete content.type; delete content.about; cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content); } } cache.last_row_id = max_row_id; await tfrpc.rpc.databaseSet('about', JSON.stringify(cache)); let users = this.users || {}; for (let id of Object.keys(cache.about)) { users[id] = Object.assign(users[id] || {}, cache.about[id]); } this.users = Object.assign({}, users); } async fetch_messages() { if (this.hash.startsWith('#@')) { return await tfrpc.rpc.query( ` SELECT messages.* FROM messages WHERE messages.author = ? ORDER BY sequence DESC LIMIT 20 `, [ this.hash.substring(1), ]); } else if (this.hash.startsWith('#%')) { return await tfrpc.rpc.query( ` SELECT messages.* FROM messages WHERE id = ? `, [ this.hash.substring(1), ]); } else { return await tfrpc.rpc.query( ` SELECT messages.* FROM messages JOIN json_each(?) AS following ON messages.author = following.value WHERE messages.timestamp > ? ORDER BY messages.timestamp DESC `, [ JSON.stringify(this.allFollowing), new Date().valueOf() - 24 * 60 * 60 * 1000, ]); } } async fetch_new_message(id) { let messages = await tfrpc.rpc.query( ` SELECT messages.* FROM messages JOIN json_each(?) AS following ON messages.author = following.value WHERE messages.id = ? `, [ JSON.stringify(this.allFollowing), id, ]); let self = this; let mine = messages.filter(m => m.author === self.whoami); if (mine.length) { this.process_messages(mine); await this.finalize_messages(); } let other = messages.filter(m => m.author !== self.whoami); if (other.length) { this.unread = [...this.unread, ...other]; } } async show_more() { let unread = this.unread; this.unread = []; this.process_messages(unread); await this.finalize_messages(); } record_status(text) { let now = new Date(); if (this.status.length) { this.status[this.status.length - 1].end_time = now; console.log( this.status[this.status.length - 1].text, (now - this.status[this.status.length - 1].start_time).valueOf()); } this.status.push({ text: text, start_time: now, }); } ensure_message(id) { let found = this.messages_by_id[id]; if (found) { return found; } else { let added = { id: id, placeholder: true, content: '"placeholder"', parent_message: undefined, child_messages: [], votes: [], }; this.messages_by_id[id] = added; return added; } } process_messages(messages) { let self = this; function link_message(message) { if (message.content.type === 'vote') { let parent = self.ensure_message(message.content.vote.link); if (!parent.votes) { parent.votes = []; } parent.votes.push(message); message.parent_message = message.content.vote.link; } else if (message.content.type == 'post') { if (message.content.root) { if (typeof(message.content.root) === 'string') { let m = self.ensure_message(message.content.root); if (!m.child_messages) { m.child_messages = []; } m.child_messages.push(message); message.parent_message = message.content.root; } else { let m = self.ensure_message(message.content.root[0]); if (!m.child_messages) { m.child_messages = []; } m.child_messages.push(message); message.parent_message = message.content.root[0]; } } } } for (let message of messages) { message.content = JSON.parse(message.content); if (!this.messages_by_id[message.id]) { this.messages_by_id[message.id] = message; link_message(message); } else if (this.messages_by_id[message.id].placeholder) { let placeholder = this.messages_by_id[message.id]; this.messages_by_id[message.id] = message; message.parent_message = placeholder.parent_message; message.child_messages = placeholder.child_messages; message.votes = placeholder.votes; if (placeholder.parent_message && this.messages_by_id[placeholder.parent_message]) { let children = this.messages_by_id[placeholder.parent_message].child_messages; children.splice(children.indexOf(placeholder), 1); children.push(message); } link_message(message); } } } async load_placeholders() { let placeholders = Object.values(this.messages_by_id).filter(x => x.placeholder).map(x => x.id); return await tfrpc.rpc.query( ` SELECT messages.* FROM messages JOIN json_each(?) AS placeholder ON messages.id = placeholder.value JOIN json_each(?) AS following ON messages.author = following.value ORDER BY messages.timestamp DESC `, [ JSON.stringify(placeholders), JSON.stringify(this.allFollowing), ]); } async finalize_messages() { this.process_messages(await this.load_placeholders()); function recursive_sort(messages, top) { if (messages) { if (top) { messages.sort((a, b) => b.timestamp - a.timestamp); } else { messages.sort((a, b) => a.timestamp - b.timestamp); } for (let message of messages) { recursive_sort(message.child_messages, false); } return messages.map(x => Object.assign({}, x)); } } this.messages = recursive_sort( Object.values(this.messages_by_id) .filter(x => !x.parent_message), true); } async load() { if (this.loading || (!this.whoami && this.ids.length)) { return; } let load_button = this.renderRoot.getElementById('load_button'); this.loading = true; if (load_button) { load_button.disabled = true; } this.status = []; this.messages = []; this.messages_by_id = {}; this.users = {}; this.allFollowing = []; console.log('loading...', this.hash); this.record_status('loading'); this.record_status('getting following'); this.allFollowing = await this.following_deep([this.whoami], 2, {}); console.log('following', this.allFollowing.length, 'identities'); this.record_status('getting about'); await this.fetch_about(this.allFollowing.sort()); this.record_status('getting messages'); this.process_messages(await this.fetch_messages()); await this.finalize_messages(); this.record_status('done'); this.status = []; if (load_button) { load_button.disabled = false; } this.loading = false; } _handle_whoami_changed(event) { this.whoami = event.srcElement.selected; this.load(); } render() { let self = this; let tabs = html`
self.tab = 'news'}> self.tab = 'connections'}>
`; let profile = this.hash.startsWith('#@') ? html`` : undefined; let news = html` 🏠Home
Welcome, !
${this.status.map(x => html`
${x.text}...${x.start_time && x.end_time ? 'took ' + Math.round(10 * (x.end_time - x.start_time) / 1000) / 10 + 's' : undefined}
`)}
${profile} ${this.messages?.map(x => html``)} `; if (this.tab === 'news') { return html`${tabs}${news}`; } else if (this.tab === 'connections') { return html` ${tabs} `; } } } customElements.define('tf-app', TfElement);