forked from cory/tildefriends
		
	
		
			
				
	
	
		
			703 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			703 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import {LitElement, html, unsafeHTML, live} from './lit-all.min.js';
 | |
| import * as tfutils from './tf-utils.js';
 | |
| import * as tfrpc from '/static/tfrpc.js';
 | |
| import {styles, generate_theme} from './tf-styles.js';
 | |
| import Tribute from './tribute.esm.js';
 | |
| 
 | |
| class TfComposeElement extends LitElement {
 | |
| 	static get properties() {
 | |
| 		return {
 | |
| 			whoami: {type: String},
 | |
| 			users: {type: Object},
 | |
| 			root: {type: String},
 | |
| 			branch: {type: String},
 | |
| 			apps: {type: Object},
 | |
| 			drafts: {type: Object},
 | |
| 			author: {type: String},
 | |
| 			channel: {type: String},
 | |
| 			new_thread: {type: Boolean},
 | |
| 			recipients: {type: Array},
 | |
| 		};
 | |
| 	}
 | |
| 
 | |
| 	static styles = styles;
 | |
| 
 | |
| 	constructor() {
 | |
| 		super();
 | |
| 		this.users = {};
 | |
| 		this.root = undefined;
 | |
| 		this.branch = undefined;
 | |
| 		this.apps = undefined;
 | |
| 		this.drafts = {};
 | |
| 		this.author = undefined;
 | |
| 		this.new_thread = false;
 | |
| 	}
 | |
| 
 | |
| 	process_text(text) {
 | |
| 		if (!text) {
 | |
| 			return '';
 | |
| 		}
 | |
| 		/* Update mentions. */
 | |
| 		let draft = this.get_draft();
 | |
| 		let updated = false;
 | |
| 		for (let match of text.matchAll(/\[([^\[]+)]\(([@&%][^\)]+)/g)) {
 | |
| 			let name = match[1];
 | |
| 			let link = match[2];
 | |
| 			let balance = 0;
 | |
| 			let bracket_end = match.index + match[1].length + '[]'.length - 1;
 | |
| 			for (let i = bracket_end; i >= 0; i--) {
 | |
| 				if (text.charAt(i) == ']') {
 | |
| 					balance++;
 | |
| 				} else if (text.charAt(i) == '[') {
 | |
| 					balance--;
 | |
| 				}
 | |
| 				if (balance <= 0) {
 | |
| 					name = text.substring(i + 1, bracket_end);
 | |
| 					break;
 | |
| 				}
 | |
| 			}
 | |
| 			if (!draft.mentions) {
 | |
| 				draft.mentions = {};
 | |
| 			}
 | |
| 			if (!draft.mentions[link]) {
 | |
| 				draft.mentions[link] = {
 | |
| 					link: link,
 | |
| 				};
 | |
| 			}
 | |
| 			draft.mentions[link].name = name.startsWith('@')
 | |
| 				? name.substring(1)
 | |
| 				: name;
 | |
| 			updated = true;
 | |
| 		}
 | |
| 		if (updated) {
 | |
| 			setTimeout(() => this.notify(draft), 0);
 | |
| 		}
 | |
| 		return tfutils.markdown(text);
 | |
| 	}
 | |
| 
 | |
| 	input(event) {
 | |
| 		let edit = this.renderRoot.getElementById('edit');
 | |
| 		let preview = this.renderRoot.getElementById('preview');
 | |
| 		preview.innerHTML = this.process_text(edit.innerText);
 | |
| 		let content_warning = this.renderRoot.getElementById('content_warning');
 | |
| 		let draft = this.get_draft();
 | |
| 		draft.text = edit.innerText;
 | |
| 		draft.content_warning = content_warning?.value;
 | |
| 		setTimeout(() => this.notify(draft), 0);
 | |
| 	}
 | |
| 
 | |
| 	notify(draft) {
 | |
| 		this.dispatchEvent(
 | |
| 			new CustomEvent('tf-draft', {
 | |
| 				bubbles: true,
 | |
| 				composed: true,
 | |
| 				detail: {
 | |
| 					id:
 | |
| 						this.branch ??
 | |
| 						(this.recipients ? this.recipients.join(',') : undefined),
 | |
| 					draft: draft,
 | |
| 				},
 | |
| 			})
 | |
| 		);
 | |
| 	}
 | |
| 
 | |
| 	convert_to_format(buffer, type, mime_type) {
 | |
| 		return new Promise(function (resolve, reject) {
 | |
| 			let img = new Image();
 | |
| 			img.onload = function () {
 | |
| 				let canvas = document.createElement('canvas');
 | |
| 				let width_scale = Math.min(img.width, 1024) / img.width;
 | |
| 				let height_scale = Math.min(img.height, 1024) / img.height;
 | |
| 				let scale = Math.min(width_scale, height_scale);
 | |
| 				canvas.width = img.width * scale;
 | |
| 				canvas.height = img.height * scale;
 | |
| 				let context = canvas.getContext('2d');
 | |
| 				context.drawImage(img, 0, 0, canvas.width, canvas.height);
 | |
| 				let data_url = canvas.toDataURL(mime_type);
 | |
| 				let result = atob(data_url.split(',')[1])
 | |
| 					.split('')
 | |
| 					.map((x) => x.charCodeAt(0));
 | |
| 				resolve(result);
 | |
| 			};
 | |
| 			img.onerror = function (event) {
 | |
| 				reject(new Error('Failed to load image.'));
 | |
| 			};
 | |
| 			let raw = Array.from(new Uint8Array(buffer))
 | |
| 				.map((b) => String.fromCharCode(b))
 | |
| 				.join('');
 | |
| 			let original = `data:${type};base64,${btoa(raw)}`;
 | |
| 			img.src = original;
 | |
| 		});
 | |
| 	}
 | |
| 
 | |
| 	async add_file(file) {
 | |
| 		try {
 | |
| 			let draft = this.get_draft();
 | |
| 			let self = this;
 | |
| 			let buffer = await file.arrayBuffer();
 | |
| 			let type = file.type;
 | |
| 			if (type.startsWith('image/')) {
 | |
| 				let best_buffer;
 | |
| 				let best_type;
 | |
| 				for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
 | |
| 					let test_buffer = await self.convert_to_format(
 | |
| 						buffer,
 | |
| 						file.type,
 | |
| 						format
 | |
| 					);
 | |
| 					if (!best_buffer || test_buffer.length < best_buffer.length) {
 | |
| 						best_buffer = test_buffer;
 | |
| 						best_type = format;
 | |
| 					}
 | |
| 				}
 | |
| 				buffer = best_buffer;
 | |
| 				type = best_type;
 | |
| 			} else {
 | |
| 				buffer = Array.from(new Uint8Array(buffer));
 | |
| 			}
 | |
| 			let id = await tfrpc.rpc.store_blob(buffer);
 | |
| 			let name = type.split('/')[0] + ':' + file.name;
 | |
| 			if (!draft.mentions) {
 | |
| 				draft.mentions = {};
 | |
| 			}
 | |
| 			draft.mentions[id] = {
 | |
| 				link: id,
 | |
| 				name: name,
 | |
| 				type: type,
 | |
| 				size: buffer.length ?? buffer.byteLength,
 | |
| 			};
 | |
| 			let edit = self.renderRoot.getElementById('edit');
 | |
| 			edit.innerText += `\n`;
 | |
| 			self.input();
 | |
| 		} catch (e) {
 | |
| 			alert(e?.message);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	paste(event) {
 | |
| 		let self = this;
 | |
| 		for (let item of event.clipboardData.items) {
 | |
| 			if (item.type?.startsWith('image/')) {
 | |
| 				let file = item.getAsFile();
 | |
| 				if (!file) {
 | |
| 					continue;
 | |
| 				}
 | |
| 				self.add_file(file);
 | |
| 				break;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		event.preventDefault();
 | |
| 		document.execCommand(
 | |
| 			'insertText',
 | |
| 			false,
 | |
| 			event.clipboardData.getData('text/plain')
 | |
| 		);
 | |
| 	}
 | |
| 
 | |
| 	async submit() {
 | |
| 		let self = this;
 | |
| 		let draft = this.get_draft();
 | |
| 		let edit = this.renderRoot.getElementById('edit');
 | |
| 		let message = {
 | |
| 			type: 'post',
 | |
| 			text: edit.innerText,
 | |
| 			channel: this.channel,
 | |
| 		};
 | |
| 		if (this.root || this.branch) {
 | |
| 			message.root = this.new_thread ? (this.branch ?? this.root) : this.root;
 | |
| 			message.branch = this.branch;
 | |
| 		}
 | |
| 		let reply = Object.fromEntries(
 | |
| 			(
 | |
| 				await tfrpc.rpc.query(
 | |
| 					`
 | |
| 				SELECT messages.id, messages.author FROM messages
 | |
| 				JOIN json_each(?) AS refs ON messages.id = refs.value
 | |
| 			`,
 | |
| 					[JSON.stringify([this.root, this.branch])]
 | |
| 				)
 | |
| 			).map((row) => [row.id, row.author])
 | |
| 		);
 | |
| 		if (Object.keys(reply).length) {
 | |
| 			message.reply = reply;
 | |
| 		}
 | |
| 		if (Object.values(draft.mentions || {}).length) {
 | |
| 			message.mentions = Object.values(draft.mentions);
 | |
| 		}
 | |
| 		if (draft.content_warning !== undefined) {
 | |
| 			message.contentWarning = draft.content_warning;
 | |
| 		}
 | |
| 		console.log('Would post:', message);
 | |
| 		if (draft.encrypt_to) {
 | |
| 			let to = new Set(draft.encrypt_to);
 | |
| 			to.add(this.whoami);
 | |
| 			to = [...to];
 | |
| 			message.recps = to;
 | |
| 			console.log('message is now', message);
 | |
| 			message = await tfrpc.rpc.encrypt(
 | |
| 				this.whoami,
 | |
| 				to,
 | |
| 				JSON.stringify(message)
 | |
| 			);
 | |
| 			console.log('encrypted as', message);
 | |
| 		}
 | |
| 		try {
 | |
| 			await tfrpc.rpc.appendMessage(this.whoami, message);
 | |
| 			self.notify(undefined);
 | |
| 		} catch (error) {
 | |
| 			alert(error.message);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	discard() {
 | |
| 		this.notify(undefined);
 | |
| 	}
 | |
| 
 | |
| 	attach() {
 | |
| 		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];
 | |
| 			self.add_file(file);
 | |
| 		});
 | |
| 		document.body.appendChild(input);
 | |
| 		input.click();
 | |
| 	}
 | |
| 
 | |
| 	async autocomplete(text, callback) {
 | |
| 		this.last_autocomplete = text;
 | |
| 		let results = [];
 | |
| 		try {
 | |
| 			let rows = await tfrpc.rpc.query(
 | |
| 				`
 | |
| 				SELECT json(messages.content) AS content FROM messages_fts(?)
 | |
| 				JOIN messages ON messages.rowid = messages_fts.rowid
 | |
| 				WHERE json(messages.content) LIKE ?
 | |
| 				ORDER BY timestamp DESC LIMIT 10
 | |
| 			`,
 | |
| 				['"' + text.replace('"', '""') + '"', `%%`]
 | |
| 			);
 | |
| 			for (let row of rows) {
 | |
| 				for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
 | |
| 					if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
 | |
| 						results.push({key: match[1], value: match[2]});
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		} finally {
 | |
| 			if (this.last_autocomplete === text) {
 | |
| 				callback(results);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	get_values() {
 | |
| 		let values = Object.entries(this.users).map((x) => ({
 | |
| 			key: x[1].name ?? x[0],
 | |
| 			value: x[0],
 | |
| 		}));
 | |
| 		if (this.author) {
 | |
| 			values = [].concat(
 | |
| 				[
 | |
| 					{
 | |
| 						key: this.users[this.author]?.name,
 | |
| 						value: this.author,
 | |
| 					},
 | |
| 				],
 | |
| 				values
 | |
| 			);
 | |
| 		}
 | |
| 		return values;
 | |
| 	}
 | |
| 
 | |
| 	firstUpdated() {
 | |
| 		let tribute = new Tribute({
 | |
| 			iframe: this.shadowRoot,
 | |
| 			collection: [
 | |
| 				{
 | |
| 					values: this.get_values(),
 | |
| 					selectTemplate: function (item) {
 | |
| 						return item
 | |
| 							? `[@${item.original.key}](${item.original.value})`
 | |
| 							: undefined;
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					trigger: '&',
 | |
| 					values: this.autocomplete,
 | |
| 					selectTemplate: function (item) {
 | |
| 						return item
 | |
| 							? ``
 | |
| 							: undefined;
 | |
| 					},
 | |
| 				},
 | |
| 			],
 | |
| 		});
 | |
| 		tribute.attach(this.renderRoot.getElementById('edit'));
 | |
| 		this._tribute = tribute;
 | |
| 	}
 | |
| 
 | |
| 	updated() {
 | |
| 		super.updated();
 | |
| 		let edit = this.renderRoot.getElementById('edit');
 | |
| 		if (this.last_updated_text !== edit.innerText) {
 | |
| 			let preview = this.renderRoot.getElementById('preview');
 | |
| 			preview.innerHTML = this.process_text(edit.innerText);
 | |
| 			this.last_updated_text = edit.innerText;
 | |
| 		}
 | |
| 		this._tribute.collection[0].values = this.get_values();
 | |
| 		let encrypt = this.renderRoot.getElementById('encrypt_to');
 | |
| 		if (encrypt) {
 | |
| 			let tribute = new Tribute({
 | |
| 				iframe: this.shadowRoot,
 | |
| 				values: Object.entries(this.users).map((x) => ({
 | |
| 					key: x[1].name,
 | |
| 					value: x[0],
 | |
| 				})),
 | |
| 				selectTemplate: function (item) {
 | |
| 					return item.original.value;
 | |
| 				},
 | |
| 			});
 | |
| 			tribute.attach(encrypt);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	remove_mention(id) {
 | |
| 		let draft = this.get_draft();
 | |
| 		delete draft.mentions[id];
 | |
| 		setTimeout(() => this.notify(draft), 0);
 | |
| 	}
 | |
| 
 | |
| 	render_mention(mention) {
 | |
| 		let self = this;
 | |
| 		return html` <div style="display: flex; flex-direction: row">
 | |
| 			<div style="align-self: center; margin: 0.5em">
 | |
| 				<button
 | |
| 					class="w3-button w3-theme-d1"
 | |
| 					title="Remove ${mention.name} mention"
 | |
| 					@click=${() => self.remove_mention(mention.link)}
 | |
| 				>
 | |
| 					🚮
 | |
| 				</button>
 | |
| 			</div>
 | |
| 			<div style="display: flex; flex-direction: column">
 | |
| 				<h3>${mention.name}</h3>
 | |
| 				<div style="padding-left: 1em">
 | |
| 					${Object.entries(mention)
 | |
| 						.filter((x) => x[0] != 'name')
 | |
| 						.map(
 | |
| 							(x) =>
 | |
| 								html`<div>
 | |
| 									<span style="font-weight: bold">${x[0]}</span>: ${x[1]}
 | |
| 								</div>`
 | |
| 						)}
 | |
| 				</div>
 | |
| 			</div>
 | |
| 		</div>`;
 | |
| 	}
 | |
| 
 | |
| 	render_attach_app() {
 | |
| 		let self = this;
 | |
| 
 | |
| 		async function attach_selected_app() {
 | |
| 			let name = self.renderRoot.getElementById('select').value;
 | |
| 			let id = self.apps[name];
 | |
| 			let mentions = {};
 | |
| 			mentions[id] = {
 | |
| 				name: name,
 | |
| 				link: id,
 | |
| 				type: 'application/tildefriends',
 | |
| 			};
 | |
| 			if (name && id) {
 | |
| 				let app = JSON.parse(await tfrpc.rpc.get_blob(id));
 | |
| 				for (let entry of Object.entries(app.files)) {
 | |
| 					mentions[entry[1]] = {
 | |
| 						name: entry[0],
 | |
| 						link: entry[1],
 | |
| 					};
 | |
| 				}
 | |
| 			}
 | |
| 			let draft = self.get_draft();
 | |
| 			draft.mentions = Object.assign(draft.mentions || {}, mentions);
 | |
| 			self.requestUpdate();
 | |
| 			self.notify(draft);
 | |
| 			self.apps = null;
 | |
| 		}
 | |
| 
 | |
| 		if (this.apps) {
 | |
| 			return html`
 | |
| 				<div class="w3-card-4 w3-margin w3-padding">
 | |
| 					<select id="select" class="w3-select w3-theme-d1">
 | |
| 						${Object.keys(self.apps).map(
 | |
| 							(app) => html`<option value=${app}>${app}</option>`
 | |
| 						)}
 | |
| 					</select>
 | |
| 					<button class="w3-button w3-theme-d1" @click=${attach_selected_app}>
 | |
| 						Attach
 | |
| 					</button>
 | |
| 					<button
 | |
| 						class="w3-button w3-theme-d1"
 | |
| 						@click=${() => (this.apps = null)}
 | |
| 					>
 | |
| 						Cancel
 | |
| 					</button>
 | |
| 				</div>
 | |
| 			`;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	render_attach_app_button() {
 | |
| 		let self = this;
 | |
| 		async function attach_app() {
 | |
| 			self.apps = await tfrpc.rpc.apps();
 | |
| 		}
 | |
| 		if (!this.apps) {
 | |
| 			return html`<button
 | |
| 				class="w3-button w3-bar-item w3-theme-d1"
 | |
| 				@click=${attach_app}
 | |
| 			>
 | |
| 				Attach App
 | |
| 			</button>`;
 | |
| 		} else {
 | |
| 			return html`<button
 | |
| 				class="w3-button w3-bar-item w3-theme-d1"
 | |
| 				@click=${() => (this.apps = null)}
 | |
| 			>
 | |
| 				Discard App
 | |
| 			</button>`;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	set_content_warning(value) {
 | |
| 		let draft = this.get_draft();
 | |
| 		draft.content_warning = value;
 | |
| 		this.notify(draft);
 | |
| 		this.requestUpdate();
 | |
| 	}
 | |
| 
 | |
| 	render_content_warning() {
 | |
| 		let self = this;
 | |
| 		let draft = this.get_draft();
 | |
| 		if (draft.content_warning !== undefined) {
 | |
| 			return html`
 | |
| 				<div class="w3-container w3-padding">
 | |
| 					<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${self.input} value=${draft.content_warning}></input>
 | |
| 				</div>
 | |
| 			`;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	render_new_thread() {
 | |
| 		let self = this;
 | |
| 		if (
 | |
| 			this.root !== undefined &&
 | |
| 			this.branch !== undefined &&
 | |
| 			this.root != this.branch
 | |
| 		) {
 | |
| 			return html`
 | |
| 				<input type="checkbox" class="w3-check w3-theme-d1" id="new_thread" @change=${() => (self.new_thread = !self.new_thread)} ?checked=${self.new_thread}></input>
 | |
| 				<label for="new_thread">New Thread</label>
 | |
| 			`;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	get_draft() {
 | |
| 		let key =
 | |
| 			this.branch ||
 | |
| 			(this.recipients ? this.recipients.join(',') : undefined) ||
 | |
| 			'';
 | |
| 		let draft = this.drafts[key] || {};
 | |
| 		if (this.recipients && !draft.encrypt_to?.length) {
 | |
| 			draft.encrypt_to = [
 | |
| 				...new Set(this.recipients).union(new Set(draft.encrypt_to ?? [])),
 | |
| 			];
 | |
| 		}
 | |
| 		return draft;
 | |
| 	}
 | |
| 
 | |
| 	update_encrypt(event) {
 | |
| 		let input = event.srcElement;
 | |
| 		let matches = input.value.match(/@.*?\.ed25519/g);
 | |
| 		if (matches) {
 | |
| 			let draft = this.get_draft();
 | |
| 			let to = [...new Set(matches.concat(draft.encrypt_to))];
 | |
| 			this.set_encrypt(to);
 | |
| 			input.value = '';
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	render_encrypt() {
 | |
| 		let draft = this.get_draft();
 | |
| 		if (draft.encrypt_to === undefined) {
 | |
| 			return;
 | |
| 		}
 | |
| 		return html`
 | |
| 			<div style="display: flex; flex-direction: row; width: 100%">
 | |
| 				<label for="encrypt_to">🔐 To:</label>
 | |
| 				<input type="text" id="encrypt_to" class="w3-input w3-theme-d1 w3-border" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
 | |
| 				<button class="w3-button w3-theme-d1" @click=${() => this.set_encrypt(undefined)}>🚮</button>
 | |
| 			</div>
 | |
| 			<ul>
 | |
| 				${draft.encrypt_to.map(
 | |
| 					(x) => html`
 | |
| 					<li>
 | |
| 						<tf-user id=${x} .users=${this.users}></tf-user>
 | |
| 						<input type="button" class="w3-button w3-theme-d1" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input>
 | |
| 					</li>`
 | |
| 				)}
 | |
| 			</ul>
 | |
| 		`;
 | |
| 	}
 | |
| 
 | |
| 	set_encrypt(encrypt) {
 | |
| 		let draft = this.get_draft();
 | |
| 		draft.encrypt_to = encrypt;
 | |
| 		this.notify(draft);
 | |
| 		this.requestUpdate();
 | |
| 	}
 | |
| 
 | |
| 	toggle_menu(event) {
 | |
| 		event.srcElement.parentNode
 | |
| 			.querySelector('.w3-dropdown-content')
 | |
| 			.classList.toggle('w3-show');
 | |
| 	}
 | |
| 
 | |
| 	connectedCallback() {
 | |
| 		super.connectedCallback();
 | |
| 		this._click_callback = this.document_click.bind(this);
 | |
| 		document.body.addEventListener('mouseup', this._click_callback);
 | |
| 	}
 | |
| 
 | |
| 	disconnectedCallback() {
 | |
| 		super.disconnectedCallback();
 | |
| 		document.body.removeEventListener('mouseup', this._click_callback);
 | |
| 	}
 | |
| 
 | |
| 	document_click(event) {
 | |
| 		let content = this.renderRoot.querySelector('.w3-dropdown-content');
 | |
| 		let target = event.target;
 | |
| 		if (content && !content.contains(target)) {
 | |
| 			content.classList.remove('w3-show');
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	render() {
 | |
| 		let self = this;
 | |
| 		let draft = self.get_draft();
 | |
| 		let content_warning =
 | |
| 			draft.content_warning !== undefined
 | |
| 				? html`<div class="w3-panel w3-round-xlarge w3-theme-d2">
 | |
| 						<p id="content_warning_preview">${draft.content_warning}</p>
 | |
| 					</div>`
 | |
| 				: undefined;
 | |
| 		let encrypt =
 | |
| 			draft.encrypt_to !== undefined
 | |
| 				? undefined
 | |
| 				: html`<button
 | |
| 						class="w3-button w3-bar-item w3-theme-d1"
 | |
| 						@click=${() => this.set_encrypt([])}
 | |
| 					>
 | |
| 						🔐 Encrypt
 | |
| 					</button>`;
 | |
| 		let result = html`
 | |
| 			<style>
 | |
| 				${generate_theme()}
 | |
| 			</style>
 | |
| 			<style>
 | |
| 				.w3-input:empty::before {
 | |
| 					content: attr(placeholder);
 | |
| 				}
 | |
| 				.w3-input:empty:focus::before {
 | |
| 					content: '';
 | |
| 				}
 | |
| 			</style>
 | |
| 			<div
 | |
| 				class="w3-card-4 w3-theme-d4 w3-padding w3-margin-top w3-margin-bottom"
 | |
| 				style="box-sizing: border-box"
 | |
| 			>
 | |
| 				<header class="w3-container">
 | |
| 					${this.channel !== undefined
 | |
| 						? html`<p>To #${this.channel}:</p>`
 | |
| 						: undefined}
 | |
| 					${this.render_encrypt()}
 | |
| 				</header>
 | |
| 				<div class="w3-container" style="padding: 0 0 16px 0">
 | |
| 					<div class="w3-half">
 | |
| 						<span
 | |
| 							class="w3-input w3-theme-d1 w3-border"
 | |
| 							style="resize: vertical; width: 100%; white-space: pre-wrap"
 | |
| 							placeholder="Write a post here."
 | |
| 							id="edit"
 | |
| 							@input=${this.input}
 | |
| 							@paste=${this.paste}
 | |
| 							contenteditable="plaintext-only"
 | |
| 							.innerText=${live(draft.text ?? '')}
 | |
| 						></span>
 | |
| 					</div>
 | |
| 					<div class="w3-half w3-container">
 | |
| 						${content_warning}
 | |
| 						<p id="preview"></p>
 | |
| 					</div>
 | |
| 				</div>
 | |
| 				${Object.values(draft.mentions || {}).map((x) =>
 | |
| 					self.render_mention(x)
 | |
| 				)}
 | |
| 				<footer>
 | |
| 					${this.render_attach_app()} ${this.render_content_warning()}
 | |
| 					${this.render_new_thread()}
 | |
| 					<button
 | |
| 						class="w3-button w3-theme-d1"
 | |
| 						id="submit"
 | |
| 						@click=${this.submit}
 | |
| 					>
 | |
| 						Submit
 | |
| 					</button>
 | |
| 					<div class="w3-dropdown-click">
 | |
| 						<button class="w3-button w3-theme-d1" @click=${this.toggle_menu}>
 | |
| 							⚙️
 | |
| 						</button>
 | |
| 						<div class="w3-dropdown-content w3-bar-block">
 | |
| 							${this.get_draft().content_warning === undefined
 | |
| 								? html`
 | |
| 										<button
 | |
| 											class="w3-button w3-bar-item w3-theme-d1"
 | |
| 											@click=${() => self.set_content_warning('')}
 | |
| 										>
 | |
| 											Add Content Warning
 | |
| 										</button>
 | |
| 									`
 | |
| 								: html`
 | |
| 										<button
 | |
| 											class="w3-button w3-bar-item w3-theme-d1"
 | |
| 											@click=${() => self.set_content_warning(undefined)}
 | |
| 										>
 | |
| 											Remove Content Warning
 | |
| 										</button>
 | |
| 									`}
 | |
| 							<button
 | |
| 								class="w3-button w3-bar-item w3-theme-d1"
 | |
| 								@click=${this.attach}
 | |
| 							>
 | |
| 								Attach
 | |
| 							</button>
 | |
| 							${this.render_attach_app_button()} ${encrypt}
 | |
| 							<button
 | |
| 								class="w3-button w3-bar-item w3-theme-d1"
 | |
| 								@click=${this.discard}
 | |
| 							>
 | |
| 								Discard
 | |
| 							</button>
 | |
| 						</div>
 | |
| 					</div>
 | |
| 				</footer>
 | |
| 			</div>
 | |
| 		`;
 | |
| 		return result;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| customElements.define('tf-compose', TfComposeElement);
 |