All checks were successful
		
		
	
	Build Tilde Friends / Build-All (push) Successful in 18m1s
				
			
		
			
				
	
	
		
			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);
 |