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 | ||||
| cp -fv deps/lit/* apps/gg/ | ||||
| cp -fv deps/lit/* apps/issues/ | ||||
| cp -fv deps/lit/* apps/journal/ | ||||
| cp -fv deps/lit/* apps/sneaker/ | ||||
| cp -fv deps/lit/* apps/ssb/ | ||||
| cp -fv deps/lit/* apps/wiki/ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user