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);
 | 
			
		||||
}
 | 
			
		||||
@@ -296,6 +296,7 @@ static int _tf_command_export(const char* file, int argc, char* argv[])
 | 
			
		||||
			"/~cory/docs",
 | 
			
		||||
			"/~cory/follow",
 | 
			
		||||
			"/~cory/ssb",
 | 
			
		||||
			"/~cory/ssblit",
 | 
			
		||||
		};
 | 
			
		||||
		for (int i = 0; i < (int)_countof(k_export); i++)
 | 
			
		||||
		{
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user