I think this added following and blocking.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3975 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
		| @@ -1 +1 @@ | |||||||
| {"type":"tildefriends-app","files":{"app.js":"&rLwYqurncmnUyGeWY+FLEGS2EIJmqw2cutl1gyGiVSk=.sha256","index.md":"&082vPjenwI6mL2vXwQDVEFquyl2jW9t767sGuCFvVNA=.sha256","todo.md":"&u4lmFlYFB5zQNfVXVB8t8NMT2jFAeE8ivWfwIiiTKxQ=.sha256","structure.md":"&T+CBfT9XP6ooKFvD1ZCI9hsutqsNIamfBxtAho0HtlU=.sha256","guide.md":"&SgnGL0+rjetY2o9A2+lVRbNvHIkqKwMnZr9gXWneIlc=.sha256","id_refactor.md":"&8yoYd14gX2Z3ppktVrPYf4qR78fuwAlvrtsWkSCkWUA=.sha256","ssb.md":"&WMvYIpp4CMZACwXJlX8HMDplJ+XeJB04BYf8zasrL4g=.sha256"}} | {"type":"tildefriends-app","files":{"app.js":"&rLwYqurncmnUyGeWY+FLEGS2EIJmqw2cutl1gyGiVSk=.sha256","index.md":"&082vPjenwI6mL2vXwQDVEFquyl2jW9t767sGuCFvVNA=.sha256","todo.md":"&+z52vxpbZs5+HoLnQoDNkYt4objcPwF7F1PIwvZ3E3k=.sha256","structure.md":"&T+CBfT9XP6ooKFvD1ZCI9hsutqsNIamfBxtAho0HtlU=.sha256","guide.md":"&SgnGL0+rjetY2o9A2+lVRbNvHIkqKwMnZr9gXWneIlc=.sha256","id_refactor.md":"&8yoYd14gX2Z3ppktVrPYf4qR78fuwAlvrtsWkSCkWUA=.sha256","ssb.md":"&WMvYIpp4CMZACwXJlX8HMDplJ+XeJB04BYf8zasrL4g=.sha256"}} | ||||||
| @@ -9,7 +9,6 @@ | |||||||
| - update README | - update README | ||||||
| - update docs | - update docs | ||||||
| - audit + document API exposed to apps | - audit + document API exposed to apps | ||||||
| - emoji reaction picker |  | ||||||
| - fix weird HTTP warnings | - fix weird HTTP warnings | ||||||
| - ssb from child process? | - ssb from child process? | ||||||
| - channels | - channels | ||||||
| @@ -26,6 +25,8 @@ | |||||||
| - jwt for session tokens | - jwt for session tokens | ||||||
|  |  | ||||||
| ## Maybe Done | ## Maybe Done | ||||||
|  | - linkify https://... | ||||||
|  | - emoji reaction picker | ||||||
| - expose loads of stats | - expose loads of stats | ||||||
| - confirm posting all new messages | - confirm posting all new messages | ||||||
| - multiple identities per user, in database | - multiple identities per user, in database | ||||||
|   | |||||||
| @@ -1 +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"}} | {"type":"tildefriends-app","files":{"app.js":"&b8IFBOMDtcvY5XNtUQIUeoE+++/TO8LDp86xNFIaux8=.sha256","lit-all.min.js":"&N4A12AsifdQgwdpII0SFtG513BfoLpmPjdJ9VTDftpg=.sha256","index.html":"&WH8A5tF25xlfPDGei2TCQc2/HJFJf5DuRN1GRSYQhhk=.sha256","script.js":"&diQfpbxjgd/jSPnIoAoWT75B8Pll1I5JYXhu+/phj9k=.sha256","lit-all.min.js.map":"&oFY9wO4MnujgfGNGv4VggHc5V5JwX4C8csqKZ6KJYbE=.sha256","tf-id-picker.js":"&ewIlLZNhaHm2dztxqj2Ft38WZkNPQxYfOGBrwTDUhds=.sha256","tf-app.js":"&4Z6k1bR9LUPUZGyJTEKOqPkNKqHtnvG8ScgkFoSTf1Y=.sha256","tf-message.js":"&KE1fWTqPMZR0yIRXPBGy8u1chR6LTguSK6swo+lFgE4=.sha256","tf-user.js":"&L6+7BnBq+UOoTMO6o8+u5JFTl0UBtCPDw8bb8ppDrkA=.sha256","tf-utils.js":"&N2yKZwFnb2GbPeipgQtu6xFvezENNOgud9G7EhCQ/K0=.sha256","commonmark.min.js":"&bfBaMLU19d1p/vPBF9hlARqDX002KXG/UOfxOahZhe4=.sha256","tf-compose.js":"&oo0iWvT+c2rU91zWpBIfPePRzmU8qmSnVOm+QCQqG/I=.sha256","emojis.json":"&h3P4pez+AI4aYdsN0dJ3pbUEFR0276t9AM20caj/W/s=.sha256","emojis.js":"&htPMi2z6Bmgi3f9jCnECCDZRCHACnDRjOl1kgPm+W80=.sha256","tf-styles.js":"&BkvFkMpGyL0DYP6FISFKR4pe6ZBOp8t6tQEzWZ4IQYs=.sha256","tf-profile.js":"&vRKjsnYvOiHCQahzEfznCvP5YDwUPtltlpWf+pxwZ1Y=.sha256","commonmark-linkify.js":"&X+hNNkmSRvKY86khyAun+cXksquXbMakZdINbGbx30g=.sha256","tf-connections.js":"&YUD4n/r95AwD2fA63HHE2eQt4E/27gF+4/MYrdvoasw=.sha256"}} | ||||||
| @@ -18,6 +18,15 @@ tfrpc.register(async function databaseSet(key, value) { | |||||||
| tfrpc.register(async function getIdentities() { | tfrpc.register(async function getIdentities() { | ||||||
| 	return ssb.getIdentities(); | 	return ssb.getIdentities(); | ||||||
| }); | }); | ||||||
|  | tfrpc.register(async function getAllIdentities() { | ||||||
|  | 	return ssb.getAllIdentities(); | ||||||
|  | }); | ||||||
|  | tfrpc.register(async function getBroadcasts() { | ||||||
|  | 	return ssb.getBroadcasts(); | ||||||
|  | }); | ||||||
|  | tfrpc.register(async function getConnections() { | ||||||
|  | 	return ssb.connections(); | ||||||
|  | }); | ||||||
| tfrpc.register(async function query(sql, args) { | tfrpc.register(async function query(sql, args) { | ||||||
| 	let result = []; | 	let result = []; | ||||||
| 	await ssb.sqlStream(sql, args, function callback(row) { | 	await ssb.sqlStream(sql, args, function callback(row) { | ||||||
| @@ -46,6 +55,13 @@ tfrpc.register(async function store_blob(blob) { | |||||||
| 	} | 	} | ||||||
| 	return await ssb.blobStore(blob); | 	return await ssb.blobStore(blob); | ||||||
| }); | }); | ||||||
|  | ssb.addEventListener('broadcasts', async function() { | ||||||
|  | 	await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts()); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | core.register('onConnectionsChanged', async function() { | ||||||
|  | 	await tfrpc.rpc.set('connections', await ssb.connections()); | ||||||
|  | }); | ||||||
|  |  | ||||||
| async function main() { | async function main() { | ||||||
| 	if (typeof(database) !== 'undefined') { | 	if (typeof(database) !== 'undefined') { | ||||||
|   | |||||||
							
								
								
									
										91
									
								
								apps/cory/ssblit/commonmark-linkify.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								apps/cory/ssblit/commonmark-linkify.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | |||||||
|  | function textNode(text) { | ||||||
|  |   const node = new commonmark.Node("text", undefined); | ||||||
|  |   node.literal = text; | ||||||
|  |   return node; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function linkNode(text, url) { | ||||||
|  |   const urlNode = new commonmark.Node("link", undefined); | ||||||
|  |   urlNode.destination = url; | ||||||
|  |   urlNode.appendChild(textNode(text)); | ||||||
|  |  | ||||||
|  |   return urlNode; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function splitMatches(text, regexp) { | ||||||
|  |   // Regexp must be sticky. | ||||||
|  |   regexp = new RegExp(regexp, "gm"); | ||||||
|  |  | ||||||
|  |   let i = 0; | ||||||
|  |   const result = []; | ||||||
|  |  | ||||||
|  |   let match = regexp.exec(text); | ||||||
|  |   while (match) { | ||||||
|  |     const matchText = match[0]; | ||||||
|  |  | ||||||
|  |     if (match.index > i) { | ||||||
|  |       result.push([text.substring(i, match.index), false]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     result.push([matchText, true]); | ||||||
|  |     i = match.index + matchText.length; | ||||||
|  |  | ||||||
|  |     match = regexp.exec(text); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (i < text.length) { | ||||||
|  |     result.push([text.substring(i, text.length), false]); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return result; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const urlRegexp = new RegExp("https?://[^ ]+[^ .,]"); | ||||||
|  |  | ||||||
|  | function splitURLs(textNodes) { | ||||||
|  |   const text = textNodes.map(n => n.literal).join(""); | ||||||
|  |   const parts = splitMatches(text, urlRegexp); | ||||||
|  |  | ||||||
|  |   return parts.map(part => { | ||||||
|  |     if (part[1]) { | ||||||
|  |       return linkNode(part[0], part[0]); | ||||||
|  |     } else { | ||||||
|  |       return textNode(part[0]); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function transform(parsed) { | ||||||
|  |   const walker = parsed.walker(); | ||||||
|  |   let event; | ||||||
|  |  | ||||||
|  |   let nodes = []; | ||||||
|  |   while ((event = walker.next())) { | ||||||
|  |     const node = event.node; | ||||||
|  |     if (event.entering && node.type === "text") { | ||||||
|  |       nodes.push(node); | ||||||
|  |     } else { | ||||||
|  |       if (nodes.length > 0) { | ||||||
|  |         splitURLs(nodes) | ||||||
|  |           .reverse() | ||||||
|  |           .forEach(newNode => { | ||||||
|  |             nodes[0].insertAfter(newNode); | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |         nodes.forEach(n => n.unlink()); | ||||||
|  |         nodes = []; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (nodes.length > 0) { | ||||||
|  |     splitURLs(nodes) | ||||||
|  |       .reverse() | ||||||
|  |       .forEach(newNode => { | ||||||
|  |         nodes[0].insertAfter(newNode); | ||||||
|  |       }); | ||||||
|  |     nodes.forEach(n => n.unlink()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return parsed; | ||||||
|  | } | ||||||
| @@ -2,12 +2,14 @@ | |||||||
| <html style="color: #fff"> | <html style="color: #fff"> | ||||||
| 	<head> | 	<head> | ||||||
| 		<title>Tilde Friends</title> | 		<title>Tilde Friends</title> | ||||||
|  | 		<base target="_top"> | ||||||
| 	</head> | 	</head> | ||||||
| 	<body> | 	<body> | ||||||
| 		<h1>Tilde Friends</h1> | 		<h1>Tilde Friends</h1> | ||||||
| 		<tf-app/> | 		<tf-app/> | ||||||
| 		<script>window.litDisableBundleWarning = true;</script> | 		<script>window.litDisableBundleWarning = true;</script> | ||||||
| 		<script src="commonmark.min.js"></script> | 		<script src="commonmark.min.js"></script> | ||||||
|  | 		<script src="commonmark-linkify.js" type="module"></script> | ||||||
| 		<script src="script.js" type="module"></script> | 		<script src="script.js" type="module"></script> | ||||||
| 	</body> | 	</body> | ||||||
| </html> | </html> | ||||||
| @@ -7,3 +7,4 @@ import * as tf_message from './tf-message.js'; | |||||||
| import * as tf_user from './tf-user.js'; | import * as tf_user from './tf-user.js'; | ||||||
| import * as tf_compose from './tf-compose.js'; | import * as tf_compose from './tf-compose.js'; | ||||||
| import * as tf_profile from './tf-profile.js'; | import * as tf_profile from './tf-profile.js'; | ||||||
|  | import * as tf_connections from './tf-connections.js'; | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import {LitElement, html, css} from './lit-all.min.js'; | import {LitElement, html, css} from './lit-all.min.js'; | ||||||
| import * as tfrpc from '/static/tfrpc.js'; | import * as tfrpc from '/static/tfrpc.js'; | ||||||
|  | import {styles} from './tf-styles.js'; | ||||||
|  |  | ||||||
| class TfElement extends LitElement { | class TfElement extends LitElement { | ||||||
| 	static get properties() { | 	static get properties() { | ||||||
| @@ -12,9 +13,14 @@ class TfElement extends LitElement { | |||||||
| 			status: {type: Array}, | 			status: {type: Array}, | ||||||
| 			hash: {type: String}, | 			hash: {type: String}, | ||||||
| 			unread: {type: Array}, | 			unread: {type: Array}, | ||||||
|  | 			tab: {type: String}, | ||||||
|  | 			broadcasts: {type: Array}, | ||||||
|  | 			connections: {type: Array}, | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	static styles = styles; | ||||||
|  |  | ||||||
| 	constructor() { | 	constructor() { | ||||||
| 		super(); | 		super(); | ||||||
| 		let self = this; | 		let self = this; | ||||||
| @@ -27,7 +33,12 @@ class TfElement extends LitElement { | |||||||
| 		this.hash = '#'; | 		this.hash = '#'; | ||||||
| 		this.loading = false; | 		this.loading = false; | ||||||
| 		this.unread = []; | 		this.unread = []; | ||||||
|  | 		this.tab = 'news'; | ||||||
|  | 		this.broadcasts = []; | ||||||
|  | 		this.connections = []; | ||||||
| 		tfrpc.rpc.getIdentities().then(ids => { self.ids = ids || [] }); | 		tfrpc.rpc.getIdentities().then(ids => { self.ids = ids || [] }); | ||||||
|  | 		tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || [] }); | ||||||
|  | 		tfrpc.rpc.getConnections().then(c => { self.connections = c || [] }); | ||||||
| 		tfrpc.rpc.getHash().then(hash => self.hash = hash || '#'); | 		tfrpc.rpc.getHash().then(hash => self.hash = hash || '#'); | ||||||
| 		tfrpc.register(function hashChanged(hash) { | 		tfrpc.register(function hashChanged(hash) { | ||||||
| 			self.hash = hash; | 			self.hash = hash; | ||||||
| @@ -36,6 +47,13 @@ class TfElement extends LitElement { | |||||||
| 		tfrpc.register(async function notifyNewMessage(id) { | 		tfrpc.register(async function notifyNewMessage(id) { | ||||||
| 			await self.fetch_new_message(id); | 			await self.fetch_new_message(id); | ||||||
| 		}); | 		}); | ||||||
|  | 		tfrpc.register(function set(name, value) { | ||||||
|  | 			if (name === 'broadcasts') { | ||||||
|  | 				self.broadcasts = value; | ||||||
|  | 			} else if (name === 'connections') { | ||||||
|  | 				self.connections = value; | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	async contacts_internal(id, last_row_id, following, max_row_id) { | 	async contacts_internal(id, last_row_id, following, max_row_id) { | ||||||
| @@ -288,6 +306,9 @@ class TfElement extends LitElement { | |||||||
| 		function link_message(message) { | 		function link_message(message) { | ||||||
| 			if (message.content.type === 'vote') { | 			if (message.content.type === 'vote') { | ||||||
| 				let parent = self.ensure_message(message.content.vote.link); | 				let parent = self.ensure_message(message.content.vote.link); | ||||||
|  | 				if (!parent.votes) { | ||||||
|  | 					parent.votes = []; | ||||||
|  | 				} | ||||||
| 				parent.votes.push(message); | 				parent.votes.push(message); | ||||||
| 				message.parent_message = message.content.vote.link; | 				message.parent_message = message.content.vote.link; | ||||||
| 			} else if (message.content.type == 'post') { | 			} else if (message.content.type == 'post') { | ||||||
| @@ -373,8 +394,11 @@ class TfElement extends LitElement { | |||||||
| 		if (this.loading || (!this.whoami && this.ids.length)) { | 		if (this.loading || (!this.whoami && this.ids.length)) { | ||||||
| 			return; | 			return; | ||||||
| 		} | 		} | ||||||
|  | 		let load_button = this.renderRoot.getElementById('load_button'); | ||||||
| 		this.loading = true; | 		this.loading = true; | ||||||
| 		this.renderRoot.getElementById('load_button').disabled = true; | 		if (load_button) { | ||||||
|  | 			load_button.disabled = true; | ||||||
|  | 		} | ||||||
| 		this.status = []; | 		this.status = []; | ||||||
| 		this.messages = []; | 		this.messages = []; | ||||||
| 		this.messages_by_id = {}; | 		this.messages_by_id = {}; | ||||||
| @@ -392,7 +416,9 @@ class TfElement extends LitElement { | |||||||
| 		await this.finalize_messages(); | 		await this.finalize_messages(); | ||||||
| 		this.record_status('done'); | 		this.record_status('done'); | ||||||
| 		this.status = []; | 		this.status = []; | ||||||
| 		this.renderRoot.getElementById('load_button').disabled = false; | 		if (load_button) { | ||||||
|  | 			load_button.disabled = false; | ||||||
|  | 		} | ||||||
| 		this.loading = false; | 		this.loading = false; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -402,9 +428,16 @@ class TfElement extends LitElement { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	render() { | 	render() { | ||||||
|  | 		let self = this; | ||||||
|  | 		let tabs = html` | ||||||
|  | 			<div> | ||||||
|  | 				<input type="button" value="News" ?disabled=${self.tab == 'news'} @click=${event => self.tab = 'news'}></input> | ||||||
|  | 				<input type="button" value="Connections" ?disabled=${self.tab == 'connections'} @click=${event => self.tab = 'connections'}></input> | ||||||
|  | 			</div> | ||||||
|  | 		`; | ||||||
| 		let profile = this.hash.startsWith('#@') ? | 		let profile = this.hash.startsWith('#@') ? | ||||||
| 			html`<tf-profile id=${this.hash.substring(1)} .users=${this.users}></tf-profile>` : undefined; | 			html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined; | ||||||
| 		return html` | 		let news = html` | ||||||
| 			<tf-id-picker id="picker" .ids=${this.ids} @change=${this._handle_whoami_changed}></tf-id-picker> | 			<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> | 			<button id="load_button" @click=${this.load}>Load</button> | ||||||
| 			<a target="_top" href="#" ?hidden=${this.hash.length <= 1}>🏠Home</a> | 			<a target="_top" href="#" ?hidden=${this.hash.length <= 1}>🏠Home</a> | ||||||
| @@ -415,6 +448,14 @@ class TfElement extends LitElement { | |||||||
| 			${profile} | 			${profile} | ||||||
| 			${this.messages?.map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users}></tf-message>`)} | 			${this.messages?.map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users}></tf-message>`)} | ||||||
| 		`; | 		`; | ||||||
|  | 		if (this.tab === 'news') { | ||||||
|  | 			return html`${tabs}${news}`; | ||||||
|  | 		} else if (this.tab === 'connections') { | ||||||
|  | 			return html` | ||||||
|  | 				${tabs} | ||||||
|  | 				<tf-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-connections> | ||||||
|  | 			`; | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										57
									
								
								apps/cory/ssblit/tf-connections.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								apps/cory/ssblit/tf-connections.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | import {LitElement, html} from './lit-all.min.js'; | ||||||
|  | import * as tfrpc from '/static/tfrpc.js'; | ||||||
|  |  | ||||||
|  | class TfConnectionsElement extends LitElement { | ||||||
|  | 	static get properties() { | ||||||
|  | 		return { | ||||||
|  | 			broadcasts: {type: Array}, | ||||||
|  | 			identities: {type: Array}, | ||||||
|  | 			connections: {type: Array}, | ||||||
|  | 			users: {type: Object}, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	constructor() { | ||||||
|  | 		super(); | ||||||
|  | 		let self = this; | ||||||
|  | 		this.broadcasts = []; | ||||||
|  | 		this.identities = []; | ||||||
|  | 		this.connections = []; | ||||||
|  | 		this.users = {}; | ||||||
|  | 		tfrpc.rpc.getAllIdentities().then(function(identities) { | ||||||
|  | 			self.identities = identities || []; | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_emit_change() { | ||||||
|  | 		let changed_event = new Event('change', { | ||||||
|  | 			srcElement: this, | ||||||
|  | 		}); | ||||||
|  | 		this.dispatchEvent(changed_event); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	changed(event) { | ||||||
|  | 		this.selected = event.srcElement.value; | ||||||
|  | 		tfrpc.rpc.localStorageSet('whoami', this.selected); | ||||||
|  | 		this._emit_change(); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	render() { | ||||||
|  | 		return html` | ||||||
|  | 			<h2>Broadcasts</h2> | ||||||
|  | 			<ul> | ||||||
|  | 				${this.broadcasts.map(x => html`<li><tf-user id=${x.pubkey} .users=${this.users}></tf-user></li>`)} | ||||||
|  | 			</ul> | ||||||
|  | 			<h2>Connections</h2> | ||||||
|  | 			<ul> | ||||||
|  | 				${this.connections.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)} | ||||||
|  | 			</ul> | ||||||
|  | 			<h2>Local Accounts</h2> | ||||||
|  | 			<ul> | ||||||
|  | 				${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)} | ||||||
|  | 			</ul> | ||||||
|  | 		`; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | customElements.define('tf-connections', TfConnectionsElement); | ||||||
| @@ -35,6 +35,10 @@ class TfMessageElement extends LitElement { | |||||||
| 		function normalize_expression(expression) { | 		function normalize_expression(expression) { | ||||||
| 			if (expression === 'Like' || !expression) { | 			if (expression === 'Like' || !expression) { | ||||||
| 				return '👍'; | 				return '👍'; | ||||||
|  | 			} else if (expression === 'Unlike') { | ||||||
|  | 				return '👎'; | ||||||
|  | 			} else if (expression === 'heart') { | ||||||
|  | 				return '❤️'; | ||||||
| 			} else { | 			} else { | ||||||
| 				return expression; | 				return expression; | ||||||
| 			} | 			} | ||||||
| @@ -132,6 +136,10 @@ class TfMessageElement extends LitElement { | |||||||
| 				unsafeHTML(tfutils.markdown(content.text)); | 				unsafeHTML(tfutils.markdown(content.text)); | ||||||
| 			return html` | 			return html` | ||||||
| 				<style> | 				<style> | ||||||
|  | 					code { | ||||||
|  | 						white-space: pre-wrap; | ||||||
|  | 						overflow-wrap: break-word; | ||||||
|  | 					} | ||||||
| 					img { | 					img { | ||||||
| 						max-width: 100%; | 						max-width: 100%; | ||||||
| 						height: auto; | 						height: auto; | ||||||
|   | |||||||
| @@ -6,8 +6,11 @@ import {styles} from './tf-styles.js'; | |||||||
| class TfProfileElement extends LitElement { | class TfProfileElement extends LitElement { | ||||||
| 	static get properties() { | 	static get properties() { | ||||||
| 		return { | 		return { | ||||||
|  | 			editing: {type: Object}, | ||||||
|  | 			whoami: {type: String}, | ||||||
| 			id: {type: String}, | 			id: {type: String}, | ||||||
| 			users: {type: Object}, | 			users: {type: Object}, | ||||||
|  | 			size: {type: Number}, | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -16,20 +19,160 @@ class TfProfileElement extends LitElement { | |||||||
| 	constructor() { | 	constructor() { | ||||||
| 		super(); | 		super(); | ||||||
| 		let self = this; | 		let self = this; | ||||||
|  | 		this.editing = null; | ||||||
|  | 		this.whoami = null; | ||||||
| 		this.id = null; | 		this.id = null; | ||||||
| 		this.users = {}; | 		this.users = {}; | ||||||
|  | 		this.size = 0; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	render_raw() { | 	modify(change) { | ||||||
| 		return html`<div style="white-space: pre-wrap">${JSON.stringify(this.message, null, 2)}</div>` | 		tfrpc.rpc.appendMessage(this.whoami, | ||||||
|  | 			Object.assign({ | ||||||
|  | 				type: 'contact', | ||||||
|  | 				contact: this.id, | ||||||
|  | 			}, change)).catch(function(error) { | ||||||
|  | 				alert(error?.message); | ||||||
|  | 			}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	follow() { | ||||||
|  | 		this.modify({following: true}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	unfollow() { | ||||||
|  | 		this.modify({following: false}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	block() { | ||||||
|  | 		this.modify({blocking: true}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	unblock() { | ||||||
|  | 		this.modify({blocking: false}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	edit() { | ||||||
|  | 		let original = this.users[this.id]; | ||||||
|  | 		this.editing = { | ||||||
|  | 			name: original.name, | ||||||
|  | 			description: original.description, | ||||||
|  | 			image: original.image, | ||||||
|  | 		}; | ||||||
|  | 		console.log(this.editing); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	save_edits() { | ||||||
|  | 		let self = this; | ||||||
|  | 		let message = { | ||||||
|  | 			type: 'about', | ||||||
|  | 			about: this.whoami, | ||||||
|  | 		}; | ||||||
|  | 		for (let key of Object.keys(this.editing)) { | ||||||
|  | 			if (this.editing[key] !== this.users[this.id][key]) { | ||||||
|  | 				message[key] = this.editing[key]; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		tfrpc.rpc.appendMessage(this.whoami, message).then(function() { | ||||||
|  | 			self.editing = null; | ||||||
|  | 		}).catch(function(error) { | ||||||
|  | 			alert(error?.message); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	discard_edits() { | ||||||
|  | 		this.editing = null; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	attach_image() { | ||||||
|  | 		let self = this; | ||||||
|  | 		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) { | ||||||
|  | 				self.editing = Object.assign({}, self.editing, {image: id}); | ||||||
|  | 				console.log(self.editing); | ||||||
|  | 			}).catch(function(e) { | ||||||
|  | 				alert(e.message); | ||||||
|  | 			}); | ||||||
|  | 		}; | ||||||
|  | 		input.click(); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	render() { | 	render() { | ||||||
|  | 		let self = this; | ||||||
| 		let profile = this.users[this.id] || {}; | 		let profile = this.users[this.id] || {}; | ||||||
|  | 		tfrpc.rpc.query( | ||||||
|  | 			`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`, | ||||||
|  | 			[this.id]).then(function(result) { | ||||||
|  | 				self.size = result[0].size; | ||||||
|  | 			}); | ||||||
|  | 		let edit; | ||||||
|  | 		let follow; | ||||||
|  | 		let block; | ||||||
|  | 		if (this.id === this.whoami) { | ||||||
|  | 			if (this.editing) { | ||||||
|  | 				edit = html` | ||||||
|  | 					<input type="button" value="Save Profile" @click=${this.save_edits}></input> | ||||||
|  | 					<input type="button" value="Discard" @click=${this.discard_edits}></input> | ||||||
|  | 				`; | ||||||
|  | 			} else { | ||||||
|  | 				edit = html`<input type="button" value="Edit Profile" @click=${this.edit}></input>`; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if (this.id !== this.whoami && | ||||||
|  | 			this.users[this.whoami]?.following) { | ||||||
|  | 			follow = | ||||||
|  | 				this.users[this.whoami].following[this.id] ? | ||||||
|  | 				html`<input type="button" value="Unfollow" @click=${this.unfollow}></input>` : | ||||||
|  | 				html`<input type="button" value="Follow" @click=${this.follow}></input>`; | ||||||
|  | 		} | ||||||
|  | 		if (this.id !== this.whoami && | ||||||
|  | 			this.users[this.whoami]?.blocking) { | ||||||
|  | 			block = | ||||||
|  | 				this.users[this.whoami].blocking[this.id] ? | ||||||
|  | 				html`<input type="button" value="Unblock" @click=${this.unblock}></input>` : | ||||||
|  | 				html`<input type="button" value="Block" @click=${this.block}></input>`; | ||||||
|  | 		} | ||||||
|  | 		let edit_profile = this.editing ? html` | ||||||
|  | 			<div style="flex: 1 0 50%"> | ||||||
|  | 				<div> | ||||||
|  | 					<label for="name">Name:</label> | ||||||
|  | 					<input type="text" id="name" value=${this.editing.name} @input=${event => this.editing = Object.assign({}, this.editing, {name: event.srcElement.value})}></input> | ||||||
|  | 				</div> | ||||||
|  | 				<div> | ||||||
|  | 					<div><label for="description">Description:</label></div> | ||||||
|  | 					<textarea id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea> | ||||||
|  | 				</div> | ||||||
|  | 				<input type="button" value="Attach Image" @click=${this.attach_image}></input> | ||||||
|  | 			</div>` : null; | ||||||
|  | 		let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link; | ||||||
|  | 		image = this.editing?.image ?? image; | ||||||
|  | 		let description = this.editing?.description ?? profile.description; | ||||||
| 		return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px"> | 		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> | 			<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)}) | ||||||
| 			<div><img src=${'/' + profile.image + '/view'} style="width: 256px; height: auto"></img></div> | 			<div style="display: flex; flex-direction: row"> | ||||||
| 			<div>${unsafeHTML(tfutils.markdown(profile.description))}</div> | 				${edit_profile} | ||||||
|  | 				<div style="flex: 1 0 50%"> | ||||||
|  | 					<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div> | ||||||
|  | 					<div>${unsafeHTML(tfutils.markdown(description))}</div> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 			<div> | ||||||
|  | 				Following ${Object.keys(profile.following || {}).length} identities. | ||||||
|  | 				Followed by ${Object.values(self.users).filter(x => (x.following || {})[self.id]).length} identities. | ||||||
|  | 				Blocking ${Object.keys(profile.blocking || {}).length} identities. | ||||||
|  | 				Blocked by ${Object.values(self.users).filter(x => (x.blocking || {})[self.id]).length} identities. | ||||||
|  | 			</div> | ||||||
|  | 			<div> | ||||||
|  | 				${edit} | ||||||
|  | 				${follow} | ||||||
|  | 				${block} | ||||||
|  | 			</div> | ||||||
| 		</div>`; | 		</div>`; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -24,8 +24,8 @@ class TfUserElement extends LitElement { | |||||||
| 			image = typeof(image) == 'string' ? image : image?.link; | 			image = typeof(image) == 'string' ? image : image?.link; | ||||||
| 			return html` | 			return html` | ||||||
| 				<div style="display: inline-block; font-weight: bold"> | 				<div style="display: inline-block; font-weight: bold"> | ||||||
| 					<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%" src="${'/' + image + '/view'}"> | 					<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%" ?hidden=${image === undefined} src="${image ? '/' + image + '/view' : undefined}"> | ||||||
| 					<a target="_top" href=${'#' + this.id}>${this.users[this.id].name}</a> | 					<a target="_top" href=${'#' + this.id}>${this.users[this.id].name ?? this.id}</a> | ||||||
| 				</div>`; | 				</div>`; | ||||||
| 		} else { | 		} else { | ||||||
| 			return html` | 			return html` | ||||||
|   | |||||||
| @@ -1,7 +1,10 @@ | |||||||
|  | import * as linkify from './commonmark-linkify.js'; | ||||||
|  |  | ||||||
| export function markdown(md) { | export function markdown(md) { | ||||||
| 	var reader = new commonmark.Parser({safe: true}); | 	var reader = new commonmark.Parser({safe: true}); | ||||||
| 	var writer = new commonmark.HtmlRenderer(); | 	var writer = new commonmark.HtmlRenderer(); | ||||||
| 	var parsed = reader.parse(md || ''); | 	var parsed = reader.parse(md || ''); | ||||||
|  | 	parsed = linkify.transform(parsed); | ||||||
| 	var walker = parsed.walker(); | 	var walker = parsed.walker(); | ||||||
| 	var event, node; | 	var event, node; | ||||||
| 	while ((event = walker.next())) { | 	while ((event = walker.next())) { | ||||||
| @@ -27,3 +30,17 @@ export function markdown(md) { | |||||||
| 	} | 	} | ||||||
| 	return writer.render(parsed); | 	return writer.render(parsed); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function human_readable_size(bytes) { | ||||||
|  | 	let v = bytes; | ||||||
|  | 	let u = 'B'; | ||||||
|  | 	for (let unit of ['kB', 'MB', 'GB']) { | ||||||
|  | 		if (v > 1024) { | ||||||
|  | 			v /= 1024; | ||||||
|  | 			u = unit; | ||||||
|  | 		} else { | ||||||
|  | 			break; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return `${Math.round(v * 10) / 10} ${u}`; | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user