forked from cory/tildefriends
		
	
		
			
				
	
	
		
			367 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			367 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import {LitElement, html, until, unsafeHTML} from './lit-all.min.js';
 | |
| import * as tfrpc from '/static/tfrpc.js';
 | |
| import * as tfutils from './tf-utils.js';
 | |
| import {styles, generate_theme} from './tf-styles.js';
 | |
| 
 | |
| class TfProfileElement extends LitElement {
 | |
| 	static get properties() {
 | |
| 		return {
 | |
| 			editing: {type: Object},
 | |
| 			whoami: {type: String},
 | |
| 			id: {type: String},
 | |
| 			users: {type: Object},
 | |
| 			size: {type: Number},
 | |
| 			sequence: {type: Number},
 | |
| 			following: {type: Boolean},
 | |
| 			blocking: {type: Boolean},
 | |
| 			show_followed: {type: Boolean},
 | |
| 		};
 | |
| 	}
 | |
| 
 | |
| 	static styles = styles;
 | |
| 
 | |
| 	constructor() {
 | |
| 		super();
 | |
| 		let self = this;
 | |
| 		this.editing = null;
 | |
| 		this.whoami = null;
 | |
| 		this.id = null;
 | |
| 		this.users = {};
 | |
| 		this.size = 0;
 | |
| 		this.sequence = 0;
 | |
| 	}
 | |
| 
 | |
| 	async load() {
 | |
| 		if (this.whoami !== this._follow_whoami) {
 | |
| 			this._follow_whoami = this.whoami;
 | |
| 			this.following = undefined;
 | |
| 			this.blocking = undefined;
 | |
| 
 | |
| 			let result = await tfrpc.rpc.query(
 | |
| 				`
 | |
| 				SELECT json_extract(content, '$.following') AS following
 | |
| 				FROM messages WHERE author = ? AND
 | |
| 				json_extract(content, '$.type') = 'contact' AND
 | |
| 				json_extract(content, '$.contact') = ? AND
 | |
| 				following IS NOT NULL
 | |
| 				ORDER BY sequence DESC LIMIT 1
 | |
| 			`,
 | |
| 				[this.whoami, this.id]
 | |
| 			);
 | |
| 			this.following = result?.[0]?.following ?? false;
 | |
| 			result = await tfrpc.rpc.query(
 | |
| 				`
 | |
| 				SELECT json_extract(content, '$.blocking') AS blocking
 | |
| 				FROM messages WHERE author = ? AND
 | |
| 				json_extract(content, '$.type') = 'contact' AND
 | |
| 				json_extract(content, '$.contact') = ? AND
 | |
| 				blocking IS NOT NULL
 | |
| 				ORDER BY sequence DESC LIMIT 1
 | |
| 			`,
 | |
| 				[this.whoami, this.id]
 | |
| 			);
 | |
| 			this.blocking = result?.[0]?.blocking ?? false;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	modify(change) {
 | |
| 		let self = this;
 | |
| 		tfrpc.rpc
 | |
| 			.appendMessage(
 | |
| 				this.whoami,
 | |
| 				Object.assign(
 | |
| 					{
 | |
| 						type: 'contact',
 | |
| 						contact: this.id,
 | |
| 					},
 | |
| 					change
 | |
| 				)
 | |
| 			)
 | |
| 			.then(function () {
 | |
| 				self._follow_whoami = undefined;
 | |
| 				self.load();
 | |
| 			})
 | |
| 			.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,
 | |
| 			publicWebHosting: original.publicWebHosting,
 | |
| 		};
 | |
| 		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.addEventListener('change', function (event) {
 | |
| 			input.parentNode.removeChild(input);
 | |
| 			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);
 | |
| 				});
 | |
| 		});
 | |
| 		document.body.appendChild(input);
 | |
| 		input.click();
 | |
| 	}
 | |
| 
 | |
| 	copy_id() {
 | |
| 		navigator.clipboard.writeText(this.id);
 | |
| 	}
 | |
| 
 | |
| 	show_image(link) {
 | |
| 		let div = document.createElement('div');
 | |
| 		div.style.left = 0;
 | |
| 		div.style.top = 0;
 | |
| 		div.style.width = '100%';
 | |
| 		div.style.height = '100%';
 | |
| 		div.style.position = 'fixed';
 | |
| 		div.style.background = '#000';
 | |
| 		div.style.zIndex = 100;
 | |
| 		div.style.display = 'grid';
 | |
| 		let img = document.createElement('img');
 | |
| 		img.src = link;
 | |
| 		img.style.maxWidth = '100vw';
 | |
| 		img.style.maxHeight = '100vh';
 | |
| 		img.style.display = 'block';
 | |
| 		img.style.margin = 'auto';
 | |
| 		img.style.objectFit = 'contain';
 | |
| 		img.style.width = '100vw';
 | |
| 		div.appendChild(img);
 | |
| 		function image_close(event) {
 | |
| 			document.body.removeChild(div);
 | |
| 			window.removeEventListener('keydown', image_close);
 | |
| 		}
 | |
| 		div.onclick = image_close;
 | |
| 		window.addEventListener('keydown', image_close);
 | |
| 		document.body.appendChild(div);
 | |
| 	}
 | |
| 
 | |
| 	body_click(event) {
 | |
| 		if (event.srcElement.tagName == 'IMG') {
 | |
| 			this.show_image(event.srcElement.src);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	toggle_account_list(event) {
 | |
| 		let content = event.srcElement.nextElementSibling;
 | |
| 		this.show_followed = !this.show_followed;
 | |
| 	}
 | |
| 
 | |
| 	async load_follows() {
 | |
| 		let accounts = await tfrpc.rpc.following([this.id], 1);
 | |
| 		return html`
 | |
| 			<div class="w3-container">
 | |
| 				<button
 | |
| 					class="w3-button w3-block w3-theme-d1 followed_accounts"
 | |
| 					@click=${this.toggle_account_list}
 | |
| 				>
 | |
| 					${this.show_followed ? 'Hide' : 'Show'} Followed Accounts
 | |
| 					(${Object.keys(accounts).length})
 | |
| 				</button>
 | |
| 				<div class=${'w3-card' + (this.show_followed ? '' : ' w3-hide')}>
 | |
| 					<ul class="w3-ul w3-theme-d4 w3-border-theme">
 | |
| 						${Object.keys(accounts).map(
 | |
| 							(x) => html`
 | |
| 								<li class="w3-border-theme">
 | |
| 									<tf-user id=${x} .users=${this.users}></tf-user>
 | |
| 								</li>
 | |
| 							`
 | |
| 						)}
 | |
| 					</ul>
 | |
| 				</div>
 | |
| 			</div>
 | |
| 		`;
 | |
| 	}
 | |
| 
 | |
| 	render() {
 | |
| 		this.load();
 | |
| 		let self = this;
 | |
| 		let profile = this.users[this.id] || {};
 | |
| 		tfrpc.rpc
 | |
| 			.query(
 | |
| 				`SELECT SUM(LENGTH(content)) AS size, MAX(sequence) AS sequence FROM messages WHERE author = ?`,
 | |
| 				[this.id]
 | |
| 			)
 | |
| 			.then(function (result) {
 | |
| 				self.size = result[0].size;
 | |
| 				self.sequence = result[0].sequence;
 | |
| 			});
 | |
| 		let edit;
 | |
| 		let follow;
 | |
| 		let block;
 | |
| 		if (this.id === this.whoami) {
 | |
| 			if (this.editing) {
 | |
| 				edit = html`
 | |
| 					<button
 | |
| 						id="save_profile"
 | |
| 						class="w3-button w3-theme-d1"
 | |
| 						@click=${this.save_edits}
 | |
| 					>
 | |
| 						Save Profile
 | |
| 					</button>
 | |
| 					<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
 | |
| 						Discard
 | |
| 					</button>
 | |
| 				`;
 | |
| 			} else {
 | |
| 				edit = html`<button
 | |
| 					id="edit_profile"
 | |
| 					class="w3-button w3-theme-d1"
 | |
| 					@click=${this.edit}
 | |
| 				>
 | |
| 					Edit Profile
 | |
| 				</button>`;
 | |
| 			}
 | |
| 		}
 | |
| 		if (this.id !== this.whoami && this.following !== undefined) {
 | |
| 			follow = this.following
 | |
| 				? html`<button class="w3-button w3-theme-d1" @click=${this.unfollow}>
 | |
| 						Unfollow
 | |
| 					</button>`
 | |
| 				: html`<button class="w3-button w3-theme-d1" @click=${this.follow}>
 | |
| 						Follow
 | |
| 					</button>`;
 | |
| 		}
 | |
| 		if (this.id !== this.whoami && this.blocking !== undefined) {
 | |
| 			block = this.blocking
 | |
| 				? html`<button class="w3-button w3-theme-d1" @click=${this.unblock}>
 | |
| 						Unblock
 | |
| 					</button>`
 | |
| 				: html`<button class="w3-button w3-theme-d1" @click=${this.block}>
 | |
| 						Block
 | |
| 					</button>`;
 | |
| 		}
 | |
| 		let edit_profile = this.editing
 | |
| 			? html`
 | |
| 			<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
 | |
| 				<div>
 | |
| 					<label for="name">Name:</label>
 | |
| 					<input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))} placeholder="Choose a name"></input>
 | |
| 				</div>
 | |
| 				<div><label for="description">Description:</label></div>
 | |
| 				<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))} placeholder="Tell people a little bit about yourself here, if you like.">${this.editing.description}</textarea>
 | |
| 				<div>
 | |
| 					<label for="public_web_hosting">Public Web Hosting:</label>
 | |
| 					<input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
 | |
| 				</div>
 | |
| 				<div>
 | |
| 					<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
 | |
| 				</div>
 | |
| 			</div>`
 | |
| 			: null;
 | |
| 		let image = profile.image;
 | |
| 		if (typeof image == 'string' && !image.startsWith('&')) {
 | |
| 			try {
 | |
| 				image = JSON.parse(image)?.link;
 | |
| 			} catch {}
 | |
| 		}
 | |
| 		image = this.editing?.image ?? image;
 | |
| 		let description = this.editing?.description ?? profile.description;
 | |
| 		return html`
 | |
| 			<style>${generate_theme()}</style>
 | |
| 			<div class="w3-card-4 w3-container w3-theme-d3" style="box-sizing: border-box">
 | |
| 			<header class="w3-container">
 | |
| 				<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)} in ${this.sequence} messages)</p>
 | |
| 			</header>
 | |
| 			<div class="w3-container" @click=${this.body_click}>
 | |
| 				<div class="w3-margin-bottom" style="display: flex; flex-direction: row">
 | |
| 					<input type="text" class="w3-input w3-border w3-theme-d1" style="display: flex 1 1" readonly value=${this.id}></input>
 | |
| 					<button class="w3-button w3-theme-d1 w3-ripple" style="flex: 0 0 auto" @click=${this.copy_id}>Copy</button>
 | |
| 				</div>
 | |
| 				<div style="display: flex; flex-direction: row; gap: 1em">
 | |
| 					${edit_profile}
 | |
| 					<div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal">
 | |
| 						${
 | |
| 							image
 | |
| 								? html`<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>`
 | |
| 								: html`<div>
 | |
| 										<div class="w3-jumbo">😎</div>
 | |
| 										<div><i>Profile image not set.</i></div>
 | |
| 									</div>`
 | |
| 						}
 | |
| 						<div>${unsafeHTML(tfutils.markdown(description))}</div>
 | |
| 					</div>
 | |
| 				</div>
 | |
| 				<div>
 | |
| 					Following ${profile.following} identities.
 | |
| 					Followed by ${profile.followed} identities.
 | |
| 					Blocking ${profile.blocking} identities.
 | |
| 					Blocked by ${profile.blocked} identities.
 | |
| 				</div>
 | |
| 			</div>
 | |
| 			${until(this.load_follows(), html`<p>Loading accounts followed...</p>`)}
 | |
| 			<footer class="w3-container">
 | |
| 				<p>
 | |
| 					<a class="w3-button w3-theme-d1" href=${'#🔐' + (this.id != this.whoami ? this.id : '')}>
 | |
| 						Open Private Chat
 | |
| 					</a>
 | |
| 					${edit}
 | |
| 					${follow}
 | |
| 					${block}
 | |
| 				</p>
 | |
| 			</footer>
 | |
| 		</div>`;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| customElements.define('tf-profile', TfProfileElement);
 |