forked from cory/tildefriends
		
	Add ssblit to version control. It's coming along too well to risk losing it.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3972 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
		| @@ -1 +1 @@ | ||||
| {"type":"tildefriends-app","files":{"app.js":"&+LbIl429+UZeS9Nh8zO6n7pzRfWOfFF2K/Hg7Kq2HQo=.sha256"}} | ||||
| {"type":"tildefriends-app","files":{"app.js":"&3d9ABFgRwQvWsYbFv/rzimtnLDnVrWlGtdw7serFIGw=.sha256"}} | ||||
| @@ -150,7 +150,7 @@ async function main() { | ||||
| 	var whoami = await ssb.getIdentities(); | ||||
| 	var tree = ''; | ||||
| 	for (let id of whoami) { | ||||
| 	await app.setDocument(`<pre style="color: #fff">building... ${id}</pre>`); | ||||
| 		await app.setDocument(`<pre style="color: #fff">building... ${id}</pre>`); | ||||
| 		tree += await buildTree(db, id, '', 2); | ||||
| 	} | ||||
| 	await app.setDocument('<pre style="color: #fff">FOLLOWING:\n' + tree + '</pre>'); | ||||
|   | ||||
							
								
								
									
										1
									
								
								apps/cory/ssblit.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/cory/ssblit.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| {"type":"tildefriends-app","files":{"app.js":"&Y01AAZJWUjOXzzcIPHTzeEWvgrBsBgcL34QcNdOtLpA=.sha256","lit-all.min.js":"&N4A12AsifdQgwdpII0SFtG513BfoLpmPjdJ9VTDftpg=.sha256","index.html":"&NQfp81Ve+FpMPRzPS1UcoXEkn7BW+yz/XArGQbLSmPg=.sha256","script.js":"&vnCSRIvjb0kS+QOmkJP+ISB6wJdXDp/lOn6FJn2esKk=.sha256","lit-all.min.js.map":"&oFY9wO4MnujgfGNGv4VggHc5V5JwX4C8csqKZ6KJYbE=.sha256","tf-id-picker.js":"&ewIlLZNhaHm2dztxqj2Ft38WZkNPQxYfOGBrwTDUhds=.sha256","tf-app.js":"&HOqvQvHjzGv94YSqPQWVOr9fTNMVRZk+vO7Dd+/LcEA=.sha256","tf-message.js":"&E98rTMtN1Ok3gBVbe54uqv6P45wHoMicdA/+gHVP7BM=.sha256","tf-user.js":"&hsIveVMRVMRNJfrTN1hkVQgO4VdRurMATfV2EXnIk/0=.sha256","tf-utils.js":"&MPINm55jkpz2rrNbwsYl09PKGvbgL3nwgBy6CMQkSnw=.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":"&OmDTn4Bhu6kV4PzJ0wfaExyuLOO/7bPmbRNHD5yp02w=.sha256"}} | ||||
							
								
								
									
										56
									
								
								apps/cory/ssblit/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								apps/cory/ssblit/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| import * as tfrpc from '/tfrpc.js'; | ||||
|  | ||||
| let g_database; | ||||
| let g_hash; | ||||
|  | ||||
| tfrpc.register(async function localStorageGet(key) { | ||||
| 	return app.localStorageGet(key); | ||||
| }); | ||||
| tfrpc.register(async function localStorageSet(key, value) { | ||||
| 	return app.localStorageSet(key, value); | ||||
| }); | ||||
| tfrpc.register(async function databaseGet(key) { | ||||
| 	return g_database ? g_database.get(key) : undefined; | ||||
| }); | ||||
| tfrpc.register(async function databaseSet(key, value) { | ||||
| 	return g_database ? g_database.set(key, value) : undefined; | ||||
| }); | ||||
| tfrpc.register(async function getIdentities() { | ||||
| 	return ssb.getIdentities(); | ||||
| }); | ||||
| tfrpc.register(async function query(sql, args) { | ||||
| 	let result = []; | ||||
| 	await ssb.sqlStream(sql, args, function callback(row) { | ||||
| 		result.push(row); | ||||
| 	}); | ||||
| 	return result; | ||||
| }); | ||||
| tfrpc.register(async function appendMessage(id, message) { | ||||
| 	return ssb.appendMessageWithIdentity(id, message); | ||||
| }); | ||||
| core.register('message', async function message_handler(message) { | ||||
| 	if (message.event == 'hashChange') { | ||||
| 		g_hash = message.hash; | ||||
| 		await tfrpc.rpc.hashChanged(message.hash); | ||||
| 	} | ||||
| }); | ||||
| tfrpc.register(function getHash(id, message) { | ||||
| 	return g_hash; | ||||
| }); | ||||
| ssb.addEventListener('message', async function(id) { | ||||
| 	await tfrpc.rpc.notifyNewMessage(id); | ||||
| }); | ||||
| tfrpc.register(async function store_blob(blob) { | ||||
| 	if (Array.isArray(blob)) { | ||||
| 		blob = Uint8Array.from(blob); | ||||
| 	} | ||||
| 	return await ssb.blobStore(blob); | ||||
| }); | ||||
|  | ||||
| async function main() { | ||||
| 	if (typeof(database) !== 'undefined') { | ||||
| 		g_database = await database('ssb'); | ||||
| 	} | ||||
| 	await app.setDocument(utf8Decode(await getFile('index.html'))); | ||||
| } | ||||
| main(); | ||||
							
								
								
									
										1
									
								
								apps/cory/ssblit/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/cory/ssblit/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										86
									
								
								apps/cory/ssblit/emojis.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								apps/cory/ssblit/emojis.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| let g_emojis; | ||||
|  | ||||
| function get_emojis() { | ||||
| 	if (g_emojis) { | ||||
| 		return Promise.resolve(g_emojis); | ||||
| 	} | ||||
| 	return fetch('emojis.json').then(function(result) { | ||||
| 		g_emojis = result.json(); | ||||
| 		return g_emojis; | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| export function picker(callback, anchor) { | ||||
| 	get_emojis().then(function(json) { | ||||
| 		let existing = document.getElementById('emoji_picker'); | ||||
| 		if (existing) { | ||||
| 			existing.parentElement.removeChild(existing); | ||||
| 			return; | ||||
| 		} | ||||
| 		let div = document.createElement('div'); | ||||
| 		div.id = 'emoji_picker'; | ||||
| 		div.style.color = '#000'; | ||||
| 		div.style.background = '#fff'; | ||||
| 		div.style.border = '1px solid #000'; | ||||
| 		div.style.display = 'block'; | ||||
| 		div.style.position = 'absolute'; | ||||
| 		div.style.maxWidth = '16em'; | ||||
| 		div.style.maxHeight = '16em'; | ||||
| 		div.style.overflow = 'scroll'; | ||||
| 		div.style.fontWeight = 'bold'; | ||||
| 		let input = document.createElement('input'); | ||||
| 		input.type = 'text'; | ||||
| 		div.appendChild(input); | ||||
| 		let list = document.createElement('div'); | ||||
| 		div.appendChild(list); | ||||
| 		function refresh() { | ||||
| 			while (list.firstChild) { | ||||
| 				list.removeChild(list.firstChild); | ||||
| 			} | ||||
| 			let search = input.value; | ||||
| 			console.log('refresh', search); | ||||
| 			Object.entries(json).forEach(function(row) { | ||||
| 				let header = document.createElement('div'); | ||||
| 				header.appendChild(document.createTextNode(row[0])); | ||||
| 				list.appendChild(header); | ||||
| 				let any = false; | ||||
| 				for (let entry of row[1]) { | ||||
| 					if (search && | ||||
| 						search.length && | ||||
| 						entry.name.indexOf(search) == -1) { | ||||
| 						continue; | ||||
| 					} | ||||
| 					let emoji = document.createElement('span'); | ||||
| 					const k_size = '1.25em'; | ||||
| 					emoji.style.width = k_size; | ||||
| 					emoji.style.maxWidth = k_size; | ||||
| 					emoji.style.minWidth = k_size; | ||||
| 					emoji.style.height = k_size; | ||||
| 					emoji.style.maxHeight = k_size; | ||||
| 					emoji.style.minHeight = k_size; | ||||
| 					emoji.style.display = 'inline-block'; | ||||
| 					emoji.style.overflow = 'hidden'; | ||||
| 					emoji.style.cursor = 'pointer'; | ||||
| 					emoji.onclick = function() { | ||||
| 						callback(entry); | ||||
| 						div.parentElement.removeChild(div); | ||||
| 					} | ||||
| 					emoji.title = entry.name; | ||||
| 					emoji.appendChild(document.createTextNode(entry.emoji)); | ||||
| 					list.appendChild(emoji); | ||||
| 					any = true; | ||||
| 				} | ||||
| 				if (!any) { | ||||
| 					list.removeChild(header); | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 		refresh(); | ||||
| 		input.oninput = refresh; | ||||
| 		document.body.appendChild(div); | ||||
| 		div.style.position = 'fixed'; | ||||
| 		div.style.top = '50%'; | ||||
| 		div.style.left = '50%'; | ||||
| 		div.style.transform = 'translate(-50%, -50%)'; | ||||
| 	}); | ||||
| } | ||||
							
								
								
									
										15115
									
								
								apps/cory/ssblit/emojis.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15115
									
								
								apps/cory/ssblit/emojis.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										13
									
								
								apps/cory/ssblit/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								apps/cory/ssblit/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| <!DOCTYPE html> | ||||
| <html style="color: #fff"> | ||||
| 	<head> | ||||
| 		<title>Tilde Friends</title> | ||||
| 	</head> | ||||
| 	<body> | ||||
| 		<h1>Tilde Friends</h1> | ||||
| 		<tf-app/> | ||||
| 		<script>window.litDisableBundleWarning = true;</script> | ||||
| 		<script src="commonmark.min.js"></script> | ||||
| 		<script src="script.js" type="module"></script> | ||||
| 	</body> | ||||
| </html> | ||||
							
								
								
									
										133
									
								
								apps/cory/ssblit/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								apps/cory/ssblit/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								apps/cory/ssblit/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/cory/ssblit/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										9
									
								
								apps/cory/ssblit/script.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								apps/cory/ssblit/script.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import {LitElement, html} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
|  | ||||
| import * as tf_id_picker from './tf-id-picker.js'; | ||||
| 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_profile from './tf-profile.js'; | ||||
							
								
								
									
										421
									
								
								apps/cory/ssblit/tf-app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										421
									
								
								apps/cory/ssblit/tf-app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,421 @@ | ||||
| import {LitElement, html, css} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.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}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	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 = []; | ||||
| 		tfrpc.rpc.getIdentities().then(ids => { self.ids = ids || [] }); | ||||
| 		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); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	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); | ||||
| 				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; | ||||
| 		} | ||||
| 		this.loading = true; | ||||
| 		this.renderRoot.getElementById('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 = []; | ||||
| 		this.renderRoot.getElementById('load_button').disabled = false; | ||||
| 		this.loading = false; | ||||
| 	} | ||||
|  | ||||
| 	_handle_whoami_changed(event) { | ||||
| 		this.whoami = event.srcElement.selected; | ||||
| 		this.load(); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let profile = this.hash.startsWith('#@') ? | ||||
| 			html`<tf-profile id=${this.hash.substring(1)} .users=${this.users}></tf-profile>` : undefined; | ||||
| 		return 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>`)} | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-app', TfElement); | ||||
							
								
								
									
										88
									
								
								apps/cory/ssblit/tf-compose.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								apps/cory/ssblit/tf-compose.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| import {LitElement, html} from './lit-all.min.js'; | ||||
| import * as tfutils from './tf-utils.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
|  | ||||
| class TfComposeElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			whoami: {type: String}, | ||||
| 			users: {type: Object}, | ||||
| 			root: {type: String}, | ||||
| 			branch: {type: String}, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.users = {}; | ||||
| 		this.root = undefined; | ||||
| 		this.branch = undefined; | ||||
| 	} | ||||
|  | ||||
| 	changed(event) { | ||||
| 		let edit = this.renderRoot.getElementById('edit'); | ||||
| 		let preview = this.renderRoot.getElementById('preview'); | ||||
| 		preview.innerHTML = tfutils.markdown(edit.value); | ||||
| 	} | ||||
|  | ||||
| 	submit() { | ||||
| 		let self = this; | ||||
| 		let edit = this.renderRoot.getElementById('edit'); | ||||
| 		let message = { | ||||
| 			type: 'post', | ||||
| 			text: edit.value, | ||||
| 		}; | ||||
| 		if (this.root || this.branch) { | ||||
| 			message.root = this.root; | ||||
| 			message.branch = this.branch; | ||||
| 		} | ||||
| 		console.log('Would post:', message); | ||||
| 		tfrpc.rpc.appendMessage(this.whoami, message).then(function() { | ||||
| 			edit.value = ''; | ||||
| 			self.changed(); | ||||
| 		}).catch(function(error) { | ||||
| 			alert(error.message); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	discard() { | ||||
| 		let edit = this.renderRoot.getElementById('edit'); | ||||
| 		edit.value = ''; | ||||
| 		this.changed(); | ||||
| 		this.dispatchEvent(new CustomEvent('tf-discard')); | ||||
| 	} | ||||
|  | ||||
| 	attach() { | ||||
| 		let self = this; | ||||
| 		let edit = this.renderRoot.getElementById('edit'); | ||||
| 		let input = document.createElement('input'); | ||||
| 		input.type = 'file'; | ||||
| 		input.onchange = function(event) { | ||||
| 			let file = event.target.files[0]; | ||||
| 			file.arrayBuffer().then(function(buffer) { | ||||
| 				let bin = Array.from(new Uint8Array(buffer)); | ||||
| 				return tfrpc.rpc.store_blob(bin); | ||||
| 			}).then(function(id) { | ||||
| 				edit.value += `\n`; | ||||
| 				self.changed(); | ||||
| 			}).catch(function(e) { | ||||
| 				alert(e.message); | ||||
| 			}); | ||||
| 		}; | ||||
| 		input.click(); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		return html` | ||||
| 			<div style="display: flex; flex-direction: row; width: 100%"> | ||||
| 				<textarea id="edit" @input=${this.changed} style="flex: 1 0 50%"></textarea> | ||||
| 				<div id="preview" style="flex: 1 0 50%"></div> | ||||
| 			</div> | ||||
| 			<input type="button" value="Submit" @click=${this.submit}></input> | ||||
| 			<input type="button" value="Attach" @click=${this.attach}></input> | ||||
| 			<input type="button" value="Discard" @click=${this.discard}></input> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-compose', TfComposeElement); | ||||
							
								
								
									
										48
									
								
								apps/cory/ssblit/tf-id-picker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								apps/cory/ssblit/tf-id-picker.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| import {LitElement, html} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
|  | ||||
| /* | ||||
| ** Provide a list of IDs, and this lets the user pick one | ||||
| ** and updates local storage remembering the active identity. | ||||
| */ | ||||
| class TfIdentityPickerElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			ids: {type: Array}, | ||||
| 			selected: {type: String}, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		let self = this; | ||||
| 		this.ids = []; | ||||
| 		tfrpc.rpc.localStorageGet('whoami').then(function(selected) { | ||||
| 			self.selected = selected; | ||||
| 			self._emit_change(); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	_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` | ||||
| 			<select @change=${this.changed}> | ||||
| 				${this.ids.map(id => html`<option ?selected=${id == this.selected}>${id}</option>`)} | ||||
| 			</select> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-id-picker', TfIdentityPickerElement); | ||||
							
								
								
									
										164
									
								
								apps/cory/ssblit/tf-message.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								apps/cory/ssblit/tf-message.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | ||||
| import {LitElement, html, unsafeHTML} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
| import * as tfutils from './tf-utils.js'; | ||||
| import * as emojis from './emojis.js'; | ||||
| import {styles} from './tf-styles.js'; | ||||
|  | ||||
| class TfMessageElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			whoami: {type: String}, | ||||
| 			message: {type: Object}, | ||||
| 			users: {type: Object}, | ||||
| 			reply: {type: Boolean}, | ||||
| 			raw: {type: Boolean}, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	static styles = styles; | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		let self = this; | ||||
| 		this.whoami = null; | ||||
| 		this.message = {}; | ||||
| 		this.users = {}; | ||||
| 		this.reply = false; | ||||
| 		this.raw = false; | ||||
| 	} | ||||
|  | ||||
| 	show_reply() { | ||||
| 		this.reply = true; | ||||
| 	} | ||||
|  | ||||
| 	render_votes() { | ||||
| 		function normalize_expression(expression) { | ||||
| 			if (expression === 'Like' || !expression) { | ||||
| 				return '👍'; | ||||
| 			} else { | ||||
| 				return expression; | ||||
| 			} | ||||
| 		} | ||||
| 		return html`<div>${(this.message.votes || []).map(vote => html`<span title="${this.users[vote.author]?.name ?? vote.author}">${normalize_expression(vote.content.vote.expression)}</span>`)}</div>`; | ||||
| 	} | ||||
|  | ||||
| 	render_raw() { | ||||
| 		return html`<div style="white-space: pre-wrap">${JSON.stringify(this.message, null, 2)}</div>` | ||||
| 	} | ||||
|  | ||||
| 	vote(emoji) { | ||||
| 		let reaction = emoji.emoji; | ||||
| 		let message = this.message.id; | ||||
| 		if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) { | ||||
| 			tfrpc.rpc.appendMessage( | ||||
| 				this.whoami, | ||||
| 				{ | ||||
| 					type: 'vote', | ||||
| 					vote: { | ||||
| 						link: message, | ||||
| 						value: 1, | ||||
| 						expression: reaction, | ||||
| 					}, | ||||
| 				}).catch(function(error) { | ||||
| 					alert(error?.message); | ||||
| 				}); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	react(event) { | ||||
| 		emojis.picker(x => this.vote(x)); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let content = this.message?.content; | ||||
| 		let self = this; | ||||
| 		let raw_button = this.raw ? | ||||
| 				html`<input type="button" value="Message" @click=${() => self.raw = false}></input>` : | ||||
| 				html`<input type="button" value="Raw" @click=${() => self.raw = true}></input>`; | ||||
| 		function small_frame(inner) { | ||||
| 			return html` | ||||
| 				<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block"> | ||||
| 					<tf-user id=${self.message.author} .users=${self.users}></tf-user> | ||||
| 					<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span> | ||||
| 					${raw_button} | ||||
| 					${self.raw ? self.render_raw() : inner} | ||||
| 					${self.render_votes()} | ||||
| 				</div> | ||||
| 			` | ||||
| 		} | ||||
| 		if (this.message.placeholder) { | ||||
| 			return html` | ||||
| 				<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px"> | ||||
| 					${this.message.id} (placeholder) | ||||
| 					<div>${this.render_votes()}</div> | ||||
| 					${(this.message.child_messages || []).map(x => html` | ||||
| 						<tf-message .message=${x} whoami=${this.whoami} .users=${this.users}></tf-message> | ||||
| 					`)} | ||||
| 				</div>`; | ||||
| 		} else if (content.type == 'about') { | ||||
| 			return small_frame(html` | ||||
| 				<div style="font-weight: bold">Updated profile:</div> | ||||
| 				<pre style="white-space: pre-wrap">${JSON.stringify(content, null, 2)}</pre> | ||||
| 			`); | ||||
| 		} else if (content.type == 'contact') { | ||||
| 			return small_frame(html` | ||||
| 				<div> | ||||
| 					is now | ||||
| 					${ | ||||
| 						content.blocking === true ? 'blocking' : | ||||
| 						content.blocking === false ? 'unblocking' : | ||||
| 						content.following === true ? 'following' : | ||||
| 						content.following === false ? 'unfollowing' : | ||||
| 						'?' | ||||
| 					} | ||||
| 					<tf-user id=${this.message.content.contact} .users=${this.users}></tf-user> | ||||
| 				</div> | ||||
| 			`); | ||||
| 		} else if (content.type == 'post') { | ||||
| 			let reply = this.reply ? html` | ||||
| 				<tf-compose | ||||
| 					?enabled=${this.reply} | ||||
| 					whoami=${this.whoami} | ||||
| 					.users=${this.users} | ||||
| 					root=${this.message.content.root || this.message.id} | ||||
| 					branch=${this.message.id} | ||||
| 					@tf-discard=${() => this.reply = false}></tf-compose> | ||||
| 			` : html` | ||||
| 				<input type="button" value="Reply" @click=${this.show_reply}></input> | ||||
| 			`; | ||||
| 			let self = this; | ||||
| 			let body = this.raw ? | ||||
| 				this.render_raw() : | ||||
| 				unsafeHTML(tfutils.markdown(content.text)); | ||||
| 			return html` | ||||
| 				<style> | ||||
| 					img { | ||||
| 						max-width: 100%; | ||||
| 						height: auto; | ||||
| 					} | ||||
| 				</style> | ||||
| 				<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px"> | ||||
| 					<div style="display: flex; flex-direction: row"> | ||||
| 						<tf-user id=${this.message.author} .users=${this.users}></tf-user> | ||||
| 						<span style="flex: 1"></span> | ||||
| 						<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span> | ||||
| 						<span>${raw_button}</span> | ||||
| 					</div> | ||||
| 					<div>${body}</div> | ||||
| 					${this.render_votes()} | ||||
| 					<div> | ||||
| 						${reply} | ||||
| 						<input type="button" value="React" @click=${this.react}></input> | ||||
| 					</div> | ||||
| 					${(this.message.child_messages || []).map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users}></tf-message>`)} | ||||
| 				</div> | ||||
| 			`; | ||||
| 		} else if (typeof(this.message.content) == 'string') { | ||||
| 			return small_frame(html`<span>🔒</span>`); | ||||
| 		} else { | ||||
| 			return small_frame(this.render_raw()); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-message', TfMessageElement); | ||||
							
								
								
									
										37
									
								
								apps/cory/ssblit/tf-profile.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								apps/cory/ssblit/tf-profile.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| import {LitElement, html, unsafeHTML} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
| import * as tfutils from './tf-utils.js'; | ||||
| import {styles} from './tf-styles.js'; | ||||
|  | ||||
| class TfProfileElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			id: {type: String}, | ||||
| 			users: {type: Object}, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	static styles = styles; | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		let self = this; | ||||
| 		this.id = null; | ||||
| 		this.users = {}; | ||||
| 	} | ||||
|  | ||||
| 	render_raw() { | ||||
| 		return html`<div style="white-space: pre-wrap">${JSON.stringify(this.message, null, 2)}</div>` | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let profile = this.users[this.id] || {}; | ||||
| 		return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px"> | ||||
| 			<tf-user id=${this.id} .users=${this.users}></tf-user> | ||||
| 			<div><img src=${'/' + profile.image + '/view'} style="width: 256px; height: auto"></img></div> | ||||
| 			<div>${unsafeHTML(tfutils.markdown(profile.description))}</div> | ||||
| 		</div>`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-profile', TfProfileElement); | ||||
							
								
								
									
										15
									
								
								apps/cory/ssblit/tf-styles.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								apps/cory/ssblit/tf-styles.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import {css} from './lit-all.min.js'; | ||||
|  | ||||
| export let styles = css` | ||||
| a:link { | ||||
| 	color: #bbf; | ||||
| } | ||||
|  | ||||
| a:visited { | ||||
| 	color: #ddd; | ||||
| } | ||||
|  | ||||
| a:hover { | ||||
| 	color: #ddf; | ||||
| } | ||||
| `; | ||||
							
								
								
									
										39
									
								
								apps/cory/ssblit/tf-user.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								apps/cory/ssblit/tf-user.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| import {LitElement, html} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
| import {styles} from './tf-styles.js'; | ||||
|  | ||||
| class TfUserElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			id: {type: String}, | ||||
| 			users: {type: Object}, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	static styles = styles; | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.id = null; | ||||
| 		this.users = {}; | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		if (this.users[this.id]) { | ||||
| 			let image = this.users[this.id].image; | ||||
| 			image = typeof(image) == 'string' ? image : image?.link; | ||||
| 			return html` | ||||
| 				<div style="display: inline-block; font-weight: bold"> | ||||
| 					<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%" src="${'/' + image + '/view'}"> | ||||
| 					<a target="_top" href=${'#' + this.id}>${this.users[this.id].name}</a> | ||||
| 				</div>`; | ||||
| 		} else { | ||||
| 			return html` | ||||
| 				<div style="display: inline-block; font-weight: bold"> | ||||
| 					<a target="_top" href=${'#' + this.id}>${this.id}</a> | ||||
| 				</div>`; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-user', TfUserElement); | ||||
							
								
								
									
										29
									
								
								apps/cory/ssblit/tf-utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								apps/cory/ssblit/tf-utils.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| export function markdown(md) { | ||||
| 	var reader = new commonmark.Parser({safe: true}); | ||||
| 	var writer = new commonmark.HtmlRenderer(); | ||||
| 	var parsed = reader.parse(md || ''); | ||||
| 	var walker = parsed.walker(); | ||||
| 	var event, node; | ||||
| 	while ((event = walker.next())) { | ||||
| 		node = event.node; | ||||
| 		if (event.entering) { | ||||
| 			if (node.type == 'link') { | ||||
| 				if (node.destination.startsWith('@') && | ||||
| 					node.destination.endsWith('.ed25519')) { | ||||
| 					node.destination = '#' + node.destination; | ||||
| 				} else if (node.destination.startsWith('%') && | ||||
| 					node.destination.endsWith('.sha256')) { | ||||
| 					node.destination = '#' + node.destination; | ||||
| 				} else if (node.destination.startsWith('&') && | ||||
| 					node.destination.endsWith('.sha256')) { | ||||
| 					node.destination = '/' + node.destination + '/view'; | ||||
| 				} | ||||
| 			} else if (node.type == 'image') { | ||||
| 				if (node.destination.startsWith('&')) { | ||||
| 					node.destination = '/' + node.destination + '/view'; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return writer.render(parsed); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user