import {LitElement, html, unsafeHTML, until} from './lit-all.min.js'; import * as tfrpc from '/static/tfrpc.js'; import {styles} from './tf-styles.js'; class TfTabNewsFeedElement extends LitElement { static get properties() { return { whoami: {type: String}, users: {type: Object}, hash: {type: String}, following: {type: Array}, messages: {type: Array}, drafts: {type: Object}, expanded: {type: Object}, channels_unread: {type: Object}, loading: {type: Number}, time_range: {type: Array}, }; } static styles = styles; constructor() { super(); let self = this; this.whoami = null; this.users = {}; this.hash = '#'; this.following = []; this.drafts = {}; this.expanded = {}; this.channels_unread = {}; this.start_time = (new Date()).valueOf(); this.time_range = [0, 0]; } channel() { return this.hash.startsWith('##') ? this.hash.substring(2) : ''; } async fetch_messages(start_time, end_time) { if (this.hash.startsWith('#@')) { let r = await tfrpc.rpc.query( ` WITH mine AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature FROM messages WHERE messages.author = ? ORDER BY sequence DESC LIMIT 20) SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature FROM mine JOIN messages_refs ON mine.id = messages_refs.ref JOIN messages ON messages_refs.message = messages.id UNION SELECT * FROM mine `, [this.hash.substring(1)] ); return r; } else if (this.hash.startsWith('#%')) { return await tfrpc.rpc.query( ` SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature FROM messages WHERE id = ?1 UNION SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature FROM messages JOIN messages_refs ON messages.id = messages_refs.message WHERE messages_refs.ref = ?1 `, [this.hash.substring(1)] ); } else if (this.hash.startsWith('##')) { let promises = []; const k_following_limit = 256; for (let i = 0; i < this.following.length; i += k_following_limit) { promises.push( tfrpc.rpc.query( ` WITH news AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature FROM messages JOIN json_each(?) AS following ON messages.author = following.value WHERE messages.timestamp > ? AND messages.timestamp < ? AND messages.content ->> 'channel' = ? ORDER BY messages.timestamp DESC) SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature FROM news JOIN messages_refs ON news.id = messages_refs.ref JOIN messages ON messages_refs.message = messages.id UNION SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature FROM news JOIN messages_refs ON news.id = messages_refs.message JOIN messages ON messages_refs.ref = messages.id UNION SELECT news.* FROM news `, [ JSON.stringify(this.following.slice(i, i + k_following_limit)), start_time, end_time, this.hash.substring(2), ] ) ); } return [].concat(...(await Promise.all(promises))); } else { let promises = []; const k_following_limit = 256; for (let i = 0; i < this.following.length; i += k_following_limit) { promises.push( tfrpc.rpc.query( ` WITH news AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature FROM messages JOIN json_each(?) AS following ON messages.author = following.value WHERE messages.timestamp > ? AND messages.timestamp < ? ORDER BY messages.timestamp DESC) SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature FROM news JOIN messages_refs ON news.id = messages_refs.ref JOIN messages ON messages_refs.message = messages.id UNION SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature FROM news JOIN messages_refs ON news.id = messages_refs.message JOIN messages ON messages_refs.ref = messages.id UNION SELECT news.* FROM news `, [ JSON.stringify(this.following.slice(i, i + k_following_limit)), start_time, end_time, ] ) ); } return [].concat(...(await Promise.all(promises))); } } async load_more() { this.loading++; this.loading_canceled = false; try { let more = []; while (!more.length && !this.loading_canceled) { let last_start_time = this.start_time; this.start_time = last_start_time - 7 * 24 * 60 * 60 * 1000; more = await this.fetch_messages(this.start_time, last_start_time); this.time_range = [this.start_time, this.time_range[1]]; } this.messages = await this.decrypt([...more, ...this.messages]); } finally { this.loading--; } } cancel_load() { this.loading_canceled = true; } async decrypt(messages) { console.log('decrypt'); let result = []; for (let message of messages) { let content; try { content = JSON.parse(message?.content); } catch {} if (typeof content === 'string') { let decrypted; try { decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content); } catch {} if (decrypted) { try { message.decrypted = JSON.parse(decrypted); } catch { message.decrypted = decrypted; } } } result.push(message); } return result; } async load_latest() { this.loading++; let now = new Date().valueOf(); let end_time = now + 24 * 60 * 60 * 1000; let messages = []; try { messages = await this.fetch_messages(this.time_range[1], end_time); messages = await this.decrypt(messages); this.time_range = [this.time_range[0], end_time]; } finally { this.loading--; } this.messages = [...this.messages, ...messages]; console.log('done loading latest messages.'); } async load_messages() { let self = this; this.loading++; let messages = []; try { this.messages = []; this._messages_hash = this.hash; this._messages_following = this.following; let now = new Date().valueOf(); let start_time = now - 24 * 60 * 60 * 1000; this.start_time = start_time; this.time_range = [this.start_time, now + 24 * 60 * 60 * 1000]; messages = await this.fetch_messages(this.time_range[0], this.time_range[1]); messages = await this.decrypt(messages); if (!messages.length) { let more = []; while (!more.length && start_time >= 0) { let last_start_time = start_time; start_time = last_start_time - 7 * 24 * 60 * 60 * 1000; more = await this.fetch_messages(start_time, last_start_time); } this.time_range = [start_time, this.time_range[1]]; messages = await this.decrypt([...more, ...this.messages]); } } finally { this.loading--; } this.messages = messages; console.log(`loading messages done for ${self.whoami}`); } mark_all_read() { let newest = this.messages.reduce((accumulator, current) => Math.max(accumulator, current.rowid), -1); if (newest >= 0) { this.dispatchEvent(new CustomEvent('channelsetunread', { bubbles: true, composed: true, detail: { channel: this.channel(), unread: newest + 1, }, })); } } render() { if ( !this.messages || this._messages_hash !== this.hash || this._messages_following !== this.following ) { console.log( `loading messages for ${this.whoami} (following ${this.following.length})` ); this.load_messages(); } let more; if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) { more = html`
Showing ${new Date(this.time_range[0]).toLocaleDateString()} - ${new Date(this.time_range[1]).toLocaleDateString()}.
`; } return html`