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}, channels_latest: {type: Object}, loading: {type: Number}, time_range: {type: Array}, time_loading: {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.channels_latest = {}; this.start_time = new Date().valueOf(); this.time_range = [0, 0]; this.time_loading = undefined; this.loading = 0; } channel() { return this.hash.startsWith('##') ? this.hash.substring(2) : this.hash.substring(1); } async fetch_messages(start_time, end_time) { this.time_loading = [start_time, end_time]; let result; if (this.hash == '#@') { result = await tfrpc.rpc.query( ` SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature FROM messages_fts(?1) JOIN messages ON messages.rowid = messages_fts.rowid JOIN json_each(?2) AS following ON messages.author = following.value WHERE messages.author != ?1 AND messages.timestamp >= ?3 AND messages.timestamp < ?4 ORDER BY timestamp DESC limit 20 `, [ '"' + this.whoami.replace('"', '""') + '"', JSON.stringify(this.following), start_time, end_time, ] ); } else if (this.hash.startsWith('#@')) { result = 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) 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 WHERE mine.timestamp >= ?2 AND mine.timestamp < ?3 UNION SELECT * FROM mine WHERE mine.timestamp >= ?2 AND mine.timestamp < ?3 `, [this.hash.substring(1), start_time, end_time] ); } else if (this.hash.startsWith('#%')) { result = 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 messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature FROM messages_fts(?5) JOIN messages ON messages.rowid = messages_fts.rowid JOIN json_each(?1) AS following ON messages.author = following.value JOIN json_tree(messages.content, '$.mentions') AS mention ON mention.value = '#' || ?4 WHERE messages.timestamp >= ?2 AND messages.timestamp < ?3 UNION SELECT news.* FROM news `, [ JSON.stringify(this.following.slice(i, i + k_following_limit)), start_time, end_time, this.hash.substring(2), '"#' + this.hash.substring(2).replace('"', '""') + '"', ] ) ); } result = [].concat(...(await Promise.all(promises))); } else if (this.hash == '#🔐') { result = await tfrpc.rpc.query( ` SELECT messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature FROM messages JOIN json_each(?1) AS following ON messages.author = following.value WHERE messages.timestamp >= ?2 AND messages.timestamp < ?3 AND json(messages.content) LIKE '"%' ORDER BY sequence DESC `, [JSON.stringify(this.following), start_time, end_time] ); result = (await this.decrypt(result)).filter((x) => x.decrypted); } 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, ] ) ); } result = [].concat(...(await Promise.all(promises))); } this.time_loading = undefined; return result; } update_time_range_from_messages(messages) { this.time_range = [ messages.reduce( (accumulator, current) => Math.min(accumulator, current.timestamp), this.time_range[0] ), messages.reduce( (accumulator, current) => Math.max(accumulator, current.timestamp), this.time_range[1] ), ]; } 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.update_time_range_from_messages( more.filter( (x) => x.timestamp >= this.start_time && x.timestamp < last_start_time ) ); } this.messages = await this.decrypt([...more, ...this.messages]); } finally { this.loading--; } } cancel_load() { this.loading_canceled = true; } async decrypt(messages) { 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] - 24 * 60 * 60 * 1000, end_time ); messages = await this.decrypt(messages); this.update_time_range_from_messages( messages.filter( (x) => x.timestamp >= this.time_range[1] && x.timestamp < end_time ) ); } finally { this.loading--; } this.messages = Object.values( Object.fromEntries([...this.messages, ...messages].map((x) => [x.id, x])) ); 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] ); this.update_time_range_from_messages( messages.filter( (x) => x.timestamp >= this.time_range[0] && x.timestamp < 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.update_time_range_from_messages( more.filter( (x) => x.timestamp >= start_time && x.timestamp < last_start_time ) ); } messages = await this.decrypt([...more, ...this.messages]); } } finally { this.loading--; } this.messages = messages; this.time_loading = undefined; console.log(`loading messages done for ${self.whoami}`); } mark_all_read() { let newest = this.messages.reduce( (accumulator, current) => Math.max(accumulator, current.rowid), this.channels_latest[this.channel()] ?? -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('#%')) { more = html`
Showing ${new Date( this.time_loading ? Math.min(this.time_loading[0], this.time_range[0]) : this.time_range[0] ).toLocaleDateString()} - ${new Date( this.time_loading ? Math.max(this.time_loading[1], this.time_range[1]) : this.time_range[1] ).toLocaleDateString()}.
`; } return html`