forked from cory/tildefriends
		
	Trying to organize ssblit better.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3980 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
		| @@ -1 +1 @@ | ||||
| {"type":"tildefriends-app","files":{"app.js":"&b8IFBOMDtcvY5XNtUQIUeoE+++/TO8LDp86xNFIaux8=.sha256","lit-all.min.js":"&N4A12AsifdQgwdpII0SFtG513BfoLpmPjdJ9VTDftpg=.sha256","index.html":"&WH8A5tF25xlfPDGei2TCQc2/HJFJf5DuRN1GRSYQhhk=.sha256","script.js":"&diQfpbxjgd/jSPnIoAoWT75B8Pll1I5JYXhu+/phj9k=.sha256","lit-all.min.js.map":"&oFY9wO4MnujgfGNGv4VggHc5V5JwX4C8csqKZ6KJYbE=.sha256","tf-id-picker.js":"&ewIlLZNhaHm2dztxqj2Ft38WZkNPQxYfOGBrwTDUhds=.sha256","tf-app.js":"&Ss7Cw1jQkpqk5ckTTndYFtIMrEQRDK9vmMWi2nALCt0=.sha256","tf-message.js":"&KE1fWTqPMZR0yIRXPBGy8u1chR6LTguSK6swo+lFgE4=.sha256","tf-user.js":"&L6+7BnBq+UOoTMO6o8+u5JFTl0UBtCPDw8bb8ppDrkA=.sha256","tf-utils.js":"&N2yKZwFnb2GbPeipgQtu6xFvezENNOgud9G7EhCQ/K0=.sha256","commonmark.min.js":"&bfBaMLU19d1p/vPBF9hlARqDX002KXG/UOfxOahZhe4=.sha256","tf-compose.js":"&oo0iWvT+c2rU91zWpBIfPePRzmU8qmSnVOm+QCQqG/I=.sha256","emojis.json":"&h3P4pez+AI4aYdsN0dJ3pbUEFR0276t9AM20caj/W/s=.sha256","emojis.js":"&htPMi2z6Bmgi3f9jCnECCDZRCHACnDRjOl1kgPm+W80=.sha256","tf-styles.js":"&BkvFkMpGyL0DYP6FISFKR4pe6ZBOp8t6tQEzWZ4IQYs=.sha256","tf-profile.js":"&vRKjsnYvOiHCQahzEfznCvP5YDwUPtltlpWf+pxwZ1Y=.sha256","commonmark-linkify.js":"&X+hNNkmSRvKY86khyAun+cXksquXbMakZdINbGbx30g=.sha256","tf-connections.js":"&YUD4n/r95AwD2fA63HHE2eQt4E/27gF+4/MYrdvoasw=.sha256"}} | ||||
| {"type":"tildefriends-app","files":{"app.js":"&viCT+Sz8weP/j5V47w0wA4sk46HM4uy6lajX5NtoqHE=.sha256","lit-all.min.js":"&N4A12AsifdQgwdpII0SFtG513BfoLpmPjdJ9VTDftpg=.sha256","index.html":"&WH8A5tF25xlfPDGei2TCQc2/HJFJf5DuRN1GRSYQhhk=.sha256","script.js":"&G8puK9Q4MngHy3D4ppcKyT49WKbHD2OCeUcAw2ghTDE=.sha256","lit-all.min.js.map":"&oFY9wO4MnujgfGNGv4VggHc5V5JwX4C8csqKZ6KJYbE=.sha256","tf-id-picker.js":"&pg1gLK150HFai73TcmAe5E/dMpMqmbhyre/+/J4XmHo=.sha256","tf-app.js":"&Zt1x1urnzk0D9TxQvgJqOjdHelW+bei7CRupODgvAsk=.sha256","tf-message.js":"&KE1fWTqPMZR0yIRXPBGy8u1chR6LTguSK6swo+lFgE4=.sha256","tf-user.js":"&L6+7BnBq+UOoTMO6o8+u5JFTl0UBtCPDw8bb8ppDrkA=.sha256","tf-utils.js":"&N2yKZwFnb2GbPeipgQtu6xFvezENNOgud9G7EhCQ/K0=.sha256","commonmark.min.js":"&bfBaMLU19d1p/vPBF9hlARqDX002KXG/UOfxOahZhe4=.sha256","tf-compose.js":"&oo0iWvT+c2rU91zWpBIfPePRzmU8qmSnVOm+QCQqG/I=.sha256","emojis.json":"&h3P4pez+AI4aYdsN0dJ3pbUEFR0276t9AM20caj/W/s=.sha256","emojis.js":"&htPMi2z6Bmgi3f9jCnECCDZRCHACnDRjOl1kgPm+W80=.sha256","tf-styles.js":"&BkvFkMpGyL0DYP6FISFKR4pe6ZBOp8t6tQEzWZ4IQYs=.sha256","tf-profile.js":"&vRKjsnYvOiHCQahzEfznCvP5YDwUPtltlpWf+pxwZ1Y=.sha256","commonmark-linkify.js":"&X+hNNkmSRvKY86khyAun+cXksquXbMakZdINbGbx30g=.sha256","tf-tab-search.js":"&Q4bstnLzTPmCKJP+cf7FfRZJVuGAltEely4oIovUVaI=.sha256","tf-tab-news.js":"&Pc/FkHOPRPyWZi/znjquVtXeykzqqFygVuNm0dxrwlI=.sha256","tf-tab-connections.js":"&jSnF/5NmgqxRze1XQAEGOW5mPzOV1/8aCyrDRZu34IQ=.sha256","tf-news.js":"&C1dKe98kQOkClnAbGvcreC15IdlTrD9J4RFohspnsSE=.sha256"}} | ||||
| @@ -15,6 +15,9 @@ tfrpc.register(async function databaseGet(key) { | ||||
| tfrpc.register(async function databaseSet(key, value) { | ||||
| 	return g_database ? g_database.set(key, value) : undefined; | ||||
| }); | ||||
| tfrpc.register(async function createIdentity() { | ||||
| 	return ssb.createIdentity(); | ||||
| }); | ||||
| tfrpc.register(async function getIdentities() { | ||||
| 	return ssb.getIdentities(); | ||||
| }); | ||||
|   | ||||
| @@ -6,5 +6,8 @@ import * as tf_app from './tf-app.js'; | ||||
| import * as tf_message from './tf-message.js'; | ||||
| import * as tf_user from './tf-user.js'; | ||||
| import * as tf_compose from './tf-compose.js'; | ||||
| import * as tf_news from './tf-news.js'; | ||||
| import * as tf_profile from './tf-profile.js'; | ||||
| import * as tf_connections from './tf-connections.js'; | ||||
| import * as tf_tab_news from './tf-tab-news.js'; | ||||
| import * as tf_tab_search from './tf-tab-search.js'; | ||||
| import * as tf_tab_connections from './tf-tab-connections.js'; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import {LitElement, html, css} from './lit-all.min.js'; | ||||
| import {LitElement, html, css, guard, until} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
| import {styles} from './tf-styles.js'; | ||||
|  | ||||
| @@ -6,11 +6,6 @@ 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}, | ||||
| @@ -24,25 +19,16 @@ class TfElement extends LitElement { | ||||
| 	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); | ||||
| @@ -54,6 +40,9 @@ class TfElement extends LitElement { | ||||
| 				self.connections = value; | ||||
| 			} | ||||
| 		}); | ||||
| 		tfrpc.rpc.localStorageGet('whoami').then(function(value) { | ||||
| 			self.whoami = value; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	async contacts_internal(id, last_row_id, following, max_row_id) { | ||||
| @@ -85,27 +74,21 @@ class TfElement extends LitElement { | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| 	async contact(id, last_row_id, following, max_row_id) { | ||||
| 		if (this.users[id]?.following) { | ||||
| 			return this.users[id]; | ||||
| 		} | ||||
|  | ||||
| 	async contact(id, last_row_id, following, max_row_id, contact_cache) { | ||||
| 		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; | ||||
| 		contact_cache[id] = Object.assign(contact_cache[id] || {}, result); | ||||
| 		following[id] = contact_cache[id]; | ||||
| 		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))); | ||||
| 	async following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id, contact_cache) { | ||||
| 		let contacts = await Promise.all([...new Set(ids)].map(x => this.contact(x, last_row_id, following, max_row_id, contact_cache))); | ||||
| 		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) : []; | ||||
| 			let deeper = depth > 1 ? await this.following_deep_internal(found, depth - 1, Object.assign({}, contact.blocking, blocking), last_row_id, following, max_row_id, contact_cache) : []; | ||||
| 			result[id] = [id, ...found, ...deeper]; | ||||
| 		} | ||||
| 		return [...new Set(Object.values(result).flat())]; | ||||
| @@ -122,16 +105,17 @@ class TfElement extends LitElement { | ||||
| 				last_row_id: 0, | ||||
| 			}; | ||||
| 		} | ||||
| 		let contact_cache = {}; | ||||
| 		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); | ||||
| 		let result = await this.following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id, contact_cache); | ||||
| 		cache.last_row_id = max_row_id; | ||||
| 		await tfrpc.rpc.databaseSet('following', JSON.stringify(cache)); | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| 	async fetch_about(ids) { | ||||
| 	async fetch_about(ids, users) { | ||||
| 		const k_cache_version = 1; | ||||
| 		let cache = await tfrpc.rpc.databaseGet('about'); | ||||
| 		cache = cache ? JSON.parse(cache) : {}; | ||||
| @@ -191,50 +175,11 @@ class TfElement extends LitElement { | ||||
| 		} | ||||
| 		cache.last_row_id = max_row_id; | ||||
| 		await tfrpc.rpc.databaseSet('about', JSON.stringify(cache)); | ||||
| 		let users = this.users || {}; | ||||
| 		users = 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, | ||||
| 				]); | ||||
| 		} | ||||
| 		return Object.assign({}, users); | ||||
| 	} | ||||
|  | ||||
| 	async fetch_new_message(id) { | ||||
| @@ -261,199 +206,51 @@ class TfElement extends LitElement { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	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(); | ||||
| 		if (this.whoami !== event.srcElement.selected) { | ||||
| 			console.log('whoami changed', event.srcElement.selected); | ||||
| 			this.whoami = event.srcElement.selected; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async search(event) { | ||||
| 		this.messages = []; | ||||
| 		this.messages_by_id = {}; | ||||
| 		let query = this.renderRoot.getElementById('search').value; | ||||
| 		console.log('Searching...'); | ||||
| 		let results = await tfrpc.rpc.query(` | ||||
| 				SELECT messages.* | ||||
| 				FROM messages_fts(?) | ||||
| 				JOIN messages ON messages.rowid = messages_fts.rowid | ||||
| 				JOIN json_each(?) AS following ON messages.author = following.value | ||||
| 				ORDER BY timestamp DESC limit 100 | ||||
| 			`, | ||||
| 			[query, JSON.stringify(this.allFollowing)]); | ||||
| 		console.log('Done.'); | ||||
| 		this.process_messages(results); | ||||
| 		await this.finalize_messages(); | ||||
| 		this.renderRoot.getElementById('search').value = ''; | ||||
| 	async create_identity() { | ||||
| 		if (confirm("Are you sure you want to create a new identity?")) { | ||||
| 			await tfrpc.rpc.createIdentity(); | ||||
| 			this.requestUpdate(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	search_keydown(event) { | ||||
| 		if (event.keyCode == 13) { | ||||
| 			this.search(); | ||||
| 	async render_id_picker() { | ||||
| 		this._ids = this._ids || (await tfrpc.rpc.getIdentities()) || []; | ||||
| 		return html` | ||||
| 			<tf-id-picker id="picker" selected=${this.whoami} .ids=${this._ids} @change=${this._handle_whoami_changed}></tf-id-picker> | ||||
| 			<button @click=${this.create_identity}>Create Identity</button> | ||||
| 		`; | ||||
| 	} | ||||
|  | ||||
| 	async render_tab() { | ||||
| 		let following = await this.following_deep([this.whoami], 2, {}); | ||||
| 		let users = await this.fetch_about(following.sort()); | ||||
| 		if (this.tab === 'news') { | ||||
| 			return html` | ||||
| 				<tf-tab-news .following=${following} whoami=${this.whoami} .users=${users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news> | ||||
| 			`; | ||||
| 		} else if (this.tab === 'connections') { | ||||
| 			return html` | ||||
| 				<tf-tab-connections .users=${users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections> | ||||
| 			`; | ||||
| 		} else if (this.tab === 'search') { | ||||
| 			return html` | ||||
| 				<tf-tab-search .following=${following} whoami=${this.whoami} .users=${users}></tf-tab-search> | ||||
| 			`; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let self = this; | ||||
| 		let id_picker = html` | ||||
| 			${guard([this.whoami], () => until(this.render_id_picker(), html`<div>Loading...</div>`))} | ||||
| 		`; | ||||
| 		let tabs = html` | ||||
| 			<div> | ||||
| 				<input type="button" value="News" ?disabled=${self.tab == 'news'} @click=${event => self.tab = 'news'}></input> | ||||
| @@ -461,34 +258,11 @@ class TfElement extends LitElement { | ||||
| 				<input type="button" value="Search" ?disabled=${self.tab == 'search'} @click=${event => self.tab = 'search'}></input> | ||||
| 			</div> | ||||
| 		`; | ||||
| 		let profile = this.hash.startsWith('#@') ? | ||||
| 			html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined; | ||||
| 		let news = html` | ||||
| 			<tf-id-picker id="picker" .ids=${this.ids} @change=${this._handle_whoami_changed}></tf-id-picker> | ||||
| 			<button id="load_button" @click=${this.load}>Load</button> | ||||
| 			<a target="_top" href="#" ?hidden=${this.hash.length <= 1}>🏠Home</a> | ||||
| 			<div><input type="button" value=${'Show ' + this.unread.length + ' New Messages'} @click=${this.show_more}></input></div> | ||||
| 			<div>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div> | ||||
| 			<div><tf-compose whoami=${this.whoami} .users=${this.users}></tf-compose></div> | ||||
| 			<div style="font-family: monospace">${this.status.map(x => html`<div>${x.text}...${x.start_time && x.end_time ? 'took ' + Math.round(10 * (x.end_time - x.start_time) / 1000) / 10 + 's' : undefined}</div>`)}</div> | ||||
| 			${profile} | ||||
| 			${this.messages?.map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users}></tf-message>`)} | ||||
| 		return html` | ||||
| 			${id_picker} | ||||
| 			${tabs} | ||||
| 			${until(this.render_tab(), html`<div>Loading...</div>`)} | ||||
| 		`; | ||||
| 		if (this.tab === 'news') { | ||||
| 			return html`${tabs}${news}`; | ||||
| 		} else if (this.tab === 'connections') { | ||||
| 			return html` | ||||
| 				${tabs} | ||||
| 				<tf-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-connections> | ||||
| 			`; | ||||
| 		} else if (this.tab === 'search') { | ||||
| 			let search = html` | ||||
| 				<input type="text" id="search" @keydown=${this.search_keydown}></input> | ||||
| 				<input type="button" value="Search" @click=${this.search}></input> | ||||
| 				${this.messages?.map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users}></tf-message>`)} | ||||
| 			`; | ||||
| 			return html`${tabs}${search}`; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -17,10 +17,6 @@ class TfIdentityPickerElement extends LitElement { | ||||
| 		super(); | ||||
| 		let self = this; | ||||
| 		this.ids = []; | ||||
| 		tfrpc.rpc.localStorageGet('whoami').then(function(selected) { | ||||
| 			self.selected = selected; | ||||
| 			self._emit_change(); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	_emit_change() { | ||||
|   | ||||
							
								
								
									
										155
									
								
								apps/cory/ssblit/tf-news.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								apps/cory/ssblit/tf-news.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| import {LitElement, html, unsafeHTML, until} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
| import {styles} from './tf-styles.js'; | ||||
|  | ||||
| class TfNewsElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			whoami: {type: String}, | ||||
| 			users: {type: Object}, | ||||
| 			messages: {type: Array}, | ||||
| 			following: {type: Array}, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	static styles = styles; | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		let self = this; | ||||
| 		this.whoami = null; | ||||
| 		this.users = {}; | ||||
| 		this.messages = []; | ||||
| 		this.following = []; | ||||
| 	} | ||||
|  | ||||
| 	process_messages(messages, in_messages_by_id) { | ||||
| 		let self = this; | ||||
| 		let messages_by_id = Object.assign({}, in_messages_by_id || {}); | ||||
|  | ||||
| 		function ensure_message(id) { | ||||
| 			let found = messages_by_id[id]; | ||||
| 			if (found) { | ||||
| 				return found; | ||||
| 			} else { | ||||
| 				let added = { | ||||
| 					id: id, | ||||
| 					placeholder: true, | ||||
| 					content: '"placeholder"', | ||||
| 					parent_message: undefined, | ||||
| 					child_messages: [], | ||||
| 					votes: [], | ||||
| 				}; | ||||
| 				messages_by_id[id] = added; | ||||
| 				return added; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		function link_message(message) { | ||||
| 			if (message.content.type === 'vote') { | ||||
| 				let parent = 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 = 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 = 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) { | ||||
| 			try { | ||||
| 				message.content = JSON.parse(message.content); | ||||
| 			} catch { | ||||
| 			} | ||||
| 			if (!messages_by_id[message.id]) { | ||||
| 				messages_by_id[message.id] = message; | ||||
| 				link_message(message); | ||||
| 			} else if (messages_by_id[message.id].placeholder) { | ||||
| 				let placeholder = messages_by_id[message.id]; | ||||
| 				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 && messages_by_id[placeholder.parent_message]) { | ||||
| 					let children = messages_by_id[placeholder.parent_message].child_messages; | ||||
| 					children.splice(children.indexOf(placeholder), 1); | ||||
| 					children.push(message); | ||||
| 				} | ||||
| 				link_message(message); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return messages_by_id; | ||||
| 	} | ||||
|  | ||||
| 	async load_placeholders(messages_by_id) { | ||||
| 		let placeholders = Object.values(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.following), | ||||
| 				]); | ||||
| 	} | ||||
|  | ||||
| 	finalize_messages(messages_by_id) { | ||||
| 		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)); | ||||
| 			} else { | ||||
| 				return {}; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		let roots = Object.values(messages_by_id).filter(x => !x.parent_message); | ||||
| 		return recursive_sort(roots, true); | ||||
| 	} | ||||
|  | ||||
| 	async load_and_render(messages) { | ||||
| 		let messages_by_id = this.process_messages(messages); | ||||
| 		let placeholders = await this.load_placeholders(messages_by_id); | ||||
| 		messages_by_id = this.process_messages(placeholders, messages_by_id); | ||||
| 		let final_messages = this.finalize_messages(messages_by_id); | ||||
| 		return html` | ||||
| 			${final_messages.map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users}></tf-message>`)} | ||||
| 		`; | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let messages = this.load_and_render(this.messages || []); | ||||
| 		return html`${until(messages, html`<div>Loading placeholders...</div>`)}`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-news', TfNewsElement); | ||||
							
								
								
									
										57
									
								
								apps/cory/ssblit/tf-tab-connections.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								apps/cory/ssblit/tf-tab-connections.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| import {LitElement, html} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
|  | ||||
| class TfTabConnectionsElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			broadcasts: {type: Array}, | ||||
| 			identities: {type: Array}, | ||||
| 			connections: {type: Array}, | ||||
| 			users: {type: Object}, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		let self = this; | ||||
| 		this.broadcasts = []; | ||||
| 		this.identities = []; | ||||
| 		this.connections = []; | ||||
| 		this.users = {}; | ||||
| 		tfrpc.rpc.getAllIdentities().then(function(identities) { | ||||
| 			self.identities = identities || []; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	_emit_change() { | ||||
| 		let changed_event = new Event('change', { | ||||
| 			srcElement: this, | ||||
| 		}); | ||||
| 		this.dispatchEvent(changed_event); | ||||
| 	} | ||||
|  | ||||
| 	changed(event) { | ||||
| 		this.selected = event.srcElement.value; | ||||
| 		tfrpc.rpc.localStorageSet('whoami', this.selected); | ||||
| 		this._emit_change(); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		return html` | ||||
| 			<h2>Broadcasts</h2> | ||||
| 			<ul> | ||||
| 				${this.broadcasts.map(x => html`<li><tf-user id=${x.pubkey} .users=${this.users}></tf-user></li>`)} | ||||
| 			</ul> | ||||
| 			<h2>Connections</h2> | ||||
| 			<ul> | ||||
| 				${this.connections.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)} | ||||
| 			</ul> | ||||
| 			<h2>Local Accounts</h2> | ||||
| 			<ul> | ||||
| 				${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)} | ||||
| 			</ul> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-tab-connections', TfTabConnectionsElement); | ||||
							
								
								
									
										106
									
								
								apps/cory/ssblit/tf-tab-news.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								apps/cory/ssblit/tf-tab-news.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| import {LitElement, html, unsafeHTML, until} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
| import {styles} from './tf-styles.js'; | ||||
|  | ||||
| class TfTabNewsElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			whoami: {type: String}, | ||||
| 			users: {type: Object}, | ||||
| 			hash: {type: String}, | ||||
| 			unread: {type: Array}, | ||||
| 			following: {type: Array}, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	static styles = styles; | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		let self = this; | ||||
| 		this.whoami = null; | ||||
| 		this.users = {}; | ||||
| 		this.hash = '#'; | ||||
| 		this.unread = []; | ||||
| 		this.following = []; | ||||
| 		this.cache = {}; | ||||
| 	} | ||||
|  | ||||
| 	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.following), | ||||
| 					new Date().valueOf() - 24 * 60 * 60 * 1000, | ||||
| 				]); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async show_more() { | ||||
| 		let unread = this.unread; | ||||
| 		this.unread = []; | ||||
| 		this.process_messages(unread); | ||||
| 		await this.finalize_messages(); | ||||
| 	} | ||||
|  | ||||
| 	async render_news() { | ||||
| 		if (this.cache.hash !== this.hash || | ||||
| 			this.cache.whoami !== this.whoami || | ||||
| 			this.cache.users !== this.users || | ||||
| 			!this.cache.messages) { | ||||
| 			this.cache = { | ||||
| 				hash: this.hash, | ||||
| 				whoami: this.whoami, | ||||
| 				users: this.users, | ||||
| 				messages: this.fetch_messages(), | ||||
| 			}; | ||||
| 		} | ||||
| 		let messages = await this.cache.messages; | ||||
| 		return html`<tf-news whoami=${this.whoami} .users=${this.users} .messages=${messages}></tf-news>`; | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let profile = this.hash.startsWith('#@') ? | ||||
| 			html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined; | ||||
| 		return html` | ||||
| 			<div><input type="button" value=${'Show ' + this.unread.length + ' New Messages'} @click=${this.show_more}></input></div> | ||||
| 			<button id="load_button" @click=${this.load}>Load</button> | ||||
| 			<a target="_top" href="#" ?hidden=${this.hash.length <= 1}>🏠Home</a> | ||||
| 			<div>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div> | ||||
| 			<div><tf-compose whoami=${this.whoami} .users=${this.users}></tf-compose></div> | ||||
| 			${profile} | ||||
| 			${until(this.render_news(), html`<div>Loading...</div>`)} | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-tab-news', TfTabNewsElement); | ||||
							
								
								
									
										55
									
								
								apps/cory/ssblit/tf-tab-search.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								apps/cory/ssblit/tf-tab-search.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| import {LitElement, html, unsafeHTML} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
| import {styles} from './tf-styles.js'; | ||||
|  | ||||
| class TfTabSearchElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			whoami: {type: String}, | ||||
| 			users: {type: Object}, | ||||
| 			following: {type: Array}, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	static styles = styles; | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		let self = this; | ||||
| 		this.whoami = null; | ||||
| 		this.users = {}; | ||||
| 		this.following = []; | ||||
| 	} | ||||
|  | ||||
| 	async search(event) { | ||||
| 		let query = this.renderRoot.getElementById('search').value; | ||||
| 		console.log('Searching...', this.whoami, query); | ||||
| 		let results = await tfrpc.rpc.query(` | ||||
| 				SELECT messages.* | ||||
| 				FROM messages_fts(?) | ||||
| 				JOIN messages ON messages.rowid = messages_fts.rowid | ||||
| 				JOIN json_each(?) AS following ON messages.author = following.value | ||||
| 				ORDER BY timestamp DESC limit 100 | ||||
| 			`, | ||||
| 			[query, JSON.stringify(this.following)]); | ||||
| 		console.log('Done.'); | ||||
| 		this.renderRoot.getElementById('search').value = ''; | ||||
| 		this.renderRoot.getElementById('news').messages = results; | ||||
| 	} | ||||
|  | ||||
| 	search_keydown(event) { | ||||
| 		if (event.keyCode == 13) { | ||||
| 			this.search(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		return html` | ||||
| 			<input type="text" id="search" @keydown=${this.search_keydown}></input> | ||||
| 			<input type="button" value="Search" @click=${this.search}></input> | ||||
| 			<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users}></tf-news> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-tab-search', TfTabSearchElement); | ||||
		Reference in New Issue
	
	Block a user