forked from cory/tildefriends
		
	Add the journal app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4607 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
		
							
								
								
									
										5
									
								
								apps/journal.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/journal.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | { | ||||||
|  |   "type": "tildefriends-app", | ||||||
|  |   "emoji": "📝", | ||||||
|  |   "previous": "&+jnk3iO0yawpSdGlskVJK7rxGV401yzSWsholYrAymE=.sha256" | ||||||
|  | } | ||||||
							
								
								
									
										172
									
								
								apps/journal/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								apps/journal/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | |||||||
|  | import * as tfrpc from '/tfrpc.js'; | ||||||
|  |  | ||||||
|  | let g_hash; | ||||||
|  | let g_collection_notifies = {}; | ||||||
|  |  | ||||||
|  | tfrpc.register(async function getOwnerIdentities() { | ||||||
|  | 	return ssb.getOwnerIdentities(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tfrpc.register(async function getIdentities() { | ||||||
|  | 	return ssb.getIdentities(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tfrpc.register(async function query(sql, args) { | ||||||
|  | 	let result = []; | ||||||
|  | 	await ssb.sqlAsync(sql, args, function callback(row) { | ||||||
|  | 		result.push(row); | ||||||
|  | 	}); | ||||||
|  | 	return result; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | 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 following(ids, depth) { | ||||||
|  | 	return ssb.following(ids, depth); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tfrpc.register(async function appendMessage(id, message) { | ||||||
|  | 	return ssb.appendMessageWithIdentity(id, message); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tfrpc.register(async function store_blob(blob) { | ||||||
|  | 	if (Array.isArray(blob)) { | ||||||
|  | 		blob = Uint8Array.from(blob); | ||||||
|  | 	} | ||||||
|  | 	return await ssb.blobStore(blob); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tfrpc.register(async function get_blob(id) { | ||||||
|  | 	return utf8Decode(await ssb.blobGet(id)); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | let g_new_message_resolve; | ||||||
|  | let g_new_message_promise = new Promise(function(resolve, reject) { | ||||||
|  | 	g_new_message_resolve = resolve; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | function new_message() { | ||||||
|  | 	return g_new_message_promise; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ssb.addEventListener('message', function(id) { | ||||||
|  | 	let resolve = g_new_message_resolve; | ||||||
|  | 	g_new_message_promise = new Promise(function(resolve, reject) { | ||||||
|  | 		g_new_message_resolve = resolve; | ||||||
|  | 	}); | ||||||
|  | 	if (resolve) { | ||||||
|  | 		resolve(); | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | core.register('message', async function message_handler(message) { | ||||||
|  | 	if (message.event == 'hashChange') { | ||||||
|  | 		print('hash change', message.hash); | ||||||
|  | 		g_hash = message.hash; | ||||||
|  | 		await tfrpc.rpc.hash_changed(message.hash); | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tfrpc.register(function set_hash(hash) { | ||||||
|  | 	if (g_hash != hash) { | ||||||
|  | 		return app.setHash(hash); | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tfrpc.register(function get_hash(id, message) { | ||||||
|  | 	return g_hash; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tfrpc.register(async function try_decrypt(id, content) { | ||||||
|  | 	return await ssb.privateMessageDecrypt(id, content); | ||||||
|  | }); | ||||||
|  | tfrpc.register(async function encrypt(id, recipients, content) { | ||||||
|  | 	return await ssb.privateMessageEncrypt(id, recipients, content); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | async function process_message(whoami, collection, message, kind, parent) { | ||||||
|  | 	let content = JSON.parse(message.content); | ||||||
|  | 	if (typeof content == 'string') { | ||||||
|  | 		let x; | ||||||
|  | 		for (let id of whoami) { | ||||||
|  | 			x = await ssb.privateMessageDecrypt(id, content); | ||||||
|  | 			if (x) { | ||||||
|  | 				content = JSON.parse(x); | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if (!x) { | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		if (content.type !== kind || | ||||||
|  | 			(parent && content.parent !== parent)) { | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if (content?.key) { | ||||||
|  | 		if (content?.tombstone) { | ||||||
|  | 			delete collection[content.key]; | ||||||
|  | 		} else { | ||||||
|  | 			collection[content.key] = Object.assign(collection[content.key] || {}, content); | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		collection[message.id] = Object.assign(content, {id: message.id}); | ||||||
|  | 	} | ||||||
|  | 	return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) { | ||||||
|  | 	let whoami = await ssb.getIdentities(); | ||||||
|  | 	data = data ?? {}; | ||||||
|  | 	let rowid = 0; | ||||||
|  | 	await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) { | ||||||
|  | 		rowid = row.rowid; | ||||||
|  | 	}); | ||||||
|  | 	while (true) { | ||||||
|  | 		if (rowid == max_rowid) { | ||||||
|  | 			await new_message(); | ||||||
|  | 			await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) { | ||||||
|  | 				rowid = row.rowid; | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		let modified = false; | ||||||
|  | 		let rows = []; | ||||||
|  | 		await ssb.sqlAsync(` | ||||||
|  | 			SELECT messages.id, author, content, timestamp | ||||||
|  | 			FROM messages | ||||||
|  | 			JOIN json_each(?1) AS id ON messages.author = id.value | ||||||
|  | 			WHERE | ||||||
|  | 				messages.rowid > ?2 AND | ||||||
|  | 				messages.rowid <= ?3 AND | ||||||
|  | 				((json_extract(messages.content, '$.type') = ?4 AND | ||||||
|  | 				(?5 IS NULL OR json_extract(messages.content, '$.parent') = ?5)) OR | ||||||
|  | 				content LIKE '"%') | ||||||
|  | 			`, | ||||||
|  | 			[JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent], | ||||||
|  | 			function(row) { | ||||||
|  | 				rows.push(row); | ||||||
|  | 			}); | ||||||
|  | 		for (let row of rows) { | ||||||
|  | 			if (await process_message(whoami, data, row, kind, parent)) { | ||||||
|  | 				modified = true; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if (modified) { | ||||||
|  | 			break; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return [rowid, data]; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | async function main() { | ||||||
|  | 	await app.setDocument(utf8Decode(await getFile('index.html'))); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main(); | ||||||
							
								
								
									
										1
									
								
								apps/journal/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/journal/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										14
									
								
								apps/journal/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								apps/journal/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | 	<head> | ||||||
|  | 		<base target="_top"> | ||||||
|  | 	</head> | ||||||
|  | 	<body style="color: #fff"> | ||||||
|  | 		<tf-journal-app></tf-journal-app> | ||||||
|  | 		<script src="commonmark.min.js"></script> | ||||||
|  | 		<script>window.litDisableBundleWarning = true;</script> | ||||||
|  | 		<script src="tf-journal-app.js" type="module"></script> | ||||||
|  | 		<script src="tf-journal-entry.js" type="module"></script> | ||||||
|  | 		<script src="tf-id-picker.js" type="module"></script> | ||||||
|  | 	</body> | ||||||
|  | </html> | ||||||
							
								
								
									
										120
									
								
								apps/journal/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								apps/journal/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								apps/journal/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/journal/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										36
									
								
								apps/journal/tf-id-picker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								apps/journal/tf-id-picker.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | 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. | ||||||
|  | */ | ||||||
|  | class TfIdentityPickerElement extends LitElement { | ||||||
|  | 	static get properties() { | ||||||
|  | 		return { | ||||||
|  | 			ids: {type: Array}, | ||||||
|  | 			selected: {type: String}, | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	constructor() { | ||||||
|  | 		super(); | ||||||
|  | 		this.ids = []; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	changed(event) { | ||||||
|  | 		this.selected = event.srcElement.value; | ||||||
|  | 		this.dispatchEvent(new Event('change', { | ||||||
|  | 			srcElement: this, | ||||||
|  | 		})); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	render() { | ||||||
|  | 		return html` | ||||||
|  | 			<select @change=${this.changed} style="max-width: 100%"> | ||||||
|  | 				${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)} | ||||||
|  | 			</select> | ||||||
|  | 		`; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | customElements.define('tf-id-picker', TfIdentityPickerElement); | ||||||
							
								
								
									
										75
									
								
								apps/journal/tf-journal-app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								apps/journal/tf-journal-app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | |||||||
|  | import {LitElement, html, keyed, live} from './lit-all.min.js'; | ||||||
|  | import * as tfrpc from '/static/tfrpc.js'; | ||||||
|  |  | ||||||
|  | class TfJournalAppElement extends LitElement { | ||||||
|  | 	static get properties() { | ||||||
|  | 		return { | ||||||
|  | 			ids: {type: Array}, | ||||||
|  | 			owner_ids: {type: Array}, | ||||||
|  | 			whoami: {type: String}, | ||||||
|  | 			journals: {type: Object}, | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	constructor() { | ||||||
|  | 		super(); | ||||||
|  | 		this.ids = []; | ||||||
|  | 		this.owner_ids = []; | ||||||
|  | 		this.journals = {}; | ||||||
|  | 		this.load(); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	async load() { | ||||||
|  | 		this.ids = await tfrpc.rpc.getIdentities(); | ||||||
|  | 		this.whoami = await tfrpc.rpc.localStorageGet('journal_whoami'); | ||||||
|  | 		await this.read_journals(); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	async read_journals() { | ||||||
|  | 		let max_rowid; | ||||||
|  | 		let journals; | ||||||
|  | 		while (true) | ||||||
|  | 		{ | ||||||
|  | 			[max_rowid, journals] = await tfrpc.rpc.collection([this.whoami], 'journal-entry', undefined, max_rowid, journals); | ||||||
|  | 			this.journals = Object.assign({}, journals); | ||||||
|  | 			console.log('JOURNALS', this.journals); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	async on_whoami_changed(event) { | ||||||
|  | 		let new_id = event.srcElement.selected; | ||||||
|  | 		await tfrpc.rpc.localStorageSet('journal_whoami', new_id); | ||||||
|  | 		this.whoami = new_id; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	async on_journal_publish(event) { | ||||||
|  | 		let key = event.detail.key; | ||||||
|  | 		let text = event.detail.text; | ||||||
|  | 		let message = { | ||||||
|  | 			type: 'journal-entry', | ||||||
|  | 			key: key, | ||||||
|  | 			text: text, | ||||||
|  | 		}; | ||||||
|  | 		message.recps = [this.whoami]; | ||||||
|  | 		print(message); | ||||||
|  | 		message = await tfrpc.rpc.encrypt(this.whoami, message.recps, JSON.stringify(message)); | ||||||
|  | 		print(message); | ||||||
|  | 		await tfrpc.rpc.appendMessage(this.whoami, message); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	render() { | ||||||
|  | 		console.log('RENDER APP', this.journals); | ||||||
|  | 		let self = this; | ||||||
|  | 		return html` | ||||||
|  | 			<div> | ||||||
|  | 				<tf-id-picker .ids=${this.ids} selected=${this.whoami} @change=${this.on_whoami_changed}></tf-id-picker> | ||||||
|  | 			</div> | ||||||
|  | 			<tf-journal-entry | ||||||
|  | 				whoami=${this.whoami} | ||||||
|  | 				.journals=${this.journals} | ||||||
|  | 				@publish=${this.on_journal_publish}></tf-journal-entry> | ||||||
|  | 		`; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | customElements.define('tf-journal-app', TfJournalAppElement); | ||||||
							
								
								
									
										84
									
								
								apps/journal/tf-journal-entry.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								apps/journal/tf-journal-entry.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | |||||||
|  | import {LitElement, html, unsafeHTML, range} from './lit-all.min.js'; | ||||||
|  | import * as tfrpc from '/static/tfrpc.js'; | ||||||
|  |  | ||||||
|  | class TfJournalEntryElement extends LitElement { | ||||||
|  | 	static get properties() { | ||||||
|  | 		return { | ||||||
|  | 			whoami: {type: String}, | ||||||
|  | 			key: {type: String}, | ||||||
|  | 			journals: {type: Object}, | ||||||
|  | 			text: {type: String}, | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	constructor() { | ||||||
|  | 		super(); | ||||||
|  | 		this.journals = {}; | ||||||
|  | 		this.key = new Date().toISOString().split('T')[0]; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	markdown(md) { | ||||||
|  | 		var reader = new commonmark.Parser({safe: true}); | ||||||
|  | 		var writer = new commonmark.HtmlRenderer(); | ||||||
|  | 		var parsed = reader.parse(md || ''); | ||||||
|  | 		return writer.render(parsed); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	on_discard(event) { | ||||||
|  | 		this.text = undefined; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	async on_publish() { | ||||||
|  | 		console.log('publish', this.text); | ||||||
|  | 		this.dispatchEvent(new CustomEvent('publish', { | ||||||
|  | 			bubbles: true, | ||||||
|  | 			detail: { | ||||||
|  | 				key: this.shadowRoot.getElementById('date_picker').value, | ||||||
|  | 				text: this.text, | ||||||
|  | 			}, | ||||||
|  | 		})); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	back_dates(count) { | ||||||
|  | 		let now = new Date(); | ||||||
|  | 		let result = []; | ||||||
|  | 		for (let i = 0; i < count; i++) { | ||||||
|  | 			let next = new Date(now); | ||||||
|  | 			next.setDate(now.getDate() - i); | ||||||
|  | 			result.push(next.toISOString().split('T')[0]); | ||||||
|  | 		} | ||||||
|  | 		return result; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	on_edit(event) { | ||||||
|  | 		this.text = event.srcElement.value; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	on_date_change(event) { | ||||||
|  | 		this.key = event.srcElement.value; | ||||||
|  | 		this.text = this.journals[this.key]?.text; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	render() { | ||||||
|  | 		console.log('RENDER ENTRY', this.key, this.journals?.[this.key]); | ||||||
|  | 		return html` | ||||||
|  | 			<select id="date_picker" @change=${this.on_date_change}> | ||||||
|  | 				${this.back_dates(10).map(x => html` | ||||||
|  | 					<option value=${x}>${x}</option> | ||||||
|  | 				`)} | ||||||
|  | 			</select> | ||||||
|  | 			<div style="display: inline-flex; flex-direction: row"> | ||||||
|  | 				<button ?disabled=${this.text == this.journals?.[this.key]?.text} @click=${this.on_publish}>Publish</button> | ||||||
|  | 				<button @click=${this.on_discard}>Discard</button> | ||||||
|  | 			</div> | ||||||
|  | 			<div style="display: flex; flex-direction: row"> | ||||||
|  | 				<textarea | ||||||
|  | 					style="flex: 1 1; min-height: 10em" | ||||||
|  | 					@input=${this.on_edit} .value=${this.text ?? this.journals?.[this.key]?.text ?? ''}></textarea> | ||||||
|  | 				<div style="flex: 1 1">${unsafeHTML(this.markdown(this.text ?? this.journals?.[this.key]?.text))}</div> | ||||||
|  | 			</div> | ||||||
|  | 		`; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | customElements.define('tf-journal-entry', TfJournalEntryElement); | ||||||
| @@ -3,6 +3,7 @@ wget https://cdn.jsdelivr.net/gh/lit/dist@$VERSION/all/lit-all.min.js -O deps/li | |||||||
| wget https://cdn.jsdelivr.net/gh/lit/dist@$VERSION/all/lit-all.min.js.map -O deps/lit/lit-all.min.js.map | wget https://cdn.jsdelivr.net/gh/lit/dist@$VERSION/all/lit-all.min.js.map -O deps/lit/lit-all.min.js.map | ||||||
| cp -fv deps/lit/* apps/gg/ | cp -fv deps/lit/* apps/gg/ | ||||||
| cp -fv deps/lit/* apps/issues/ | cp -fv deps/lit/* apps/issues/ | ||||||
|  | cp -fv deps/lit/* apps/journal/ | ||||||
| cp -fv deps/lit/* apps/sneaker/ | cp -fv deps/lit/* apps/sneaker/ | ||||||
| cp -fv deps/lit/* apps/ssb/ | cp -fv deps/lit/* apps/ssb/ | ||||||
| cp -fv deps/lit/* apps/wiki/ | cp -fv deps/lit/* apps/wiki/ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user