import {LitElement, html, unsafeHTML} from './lit-all.min.js'; import * as tfutils from './tf-utils.js'; import * as tfrpc from '/static/tfrpc.js'; import {styles} 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}, }; } static styles = styles; constructor() { super(); this.users = {}; this.root = undefined; this.branch = undefined; this.apps = undefined; this.drafts = {}; } 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) { this.requestUpdate(); } return tfutils.markdown(text); } input(event) { let edit = this.renderRoot.getElementById('edit'); let preview = this.renderRoot.getElementById('preview'); preview.innerHTML = this.process_text(edit.value); let content_warning = this.renderRoot.getElementById('content_warning'); let content_warning_preview = this.renderRoot.getElementById('content_warning_preview'); if (content_warning && content_warning_preview) { content_warning_preview.innerText = content_warning.value; } } notify(draft) { this.dispatchEvent(new CustomEvent('tf-draft', { bubbles: true, composed: true, detail: { id: this.branch, draft: draft }, })); } change() { let draft = this.get_draft(); draft.text = this.renderRoot.getElementById('edit')?.value; draft.content_warning = this.renderRoot.getElementById('content_warning')?.value; this.notify(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.value += `\n![${name}](${id})`; self.change(); 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; } } } async submit() { let self = this; let draft = this.get_draft(); let edit = this.renderRoot.getElementById('edit'); let message = { type: 'post', text: edit.value, }; if (this.root || this.branch) { message.root = this.root; message.branch = this.branch; } 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]; console.log('encrypting to', to); message = await tfrpc.rpc.encrypt(this.whoami, to, JSON.stringify(message)); console.log('encrypted as', message); } try { await tfrpc.rpc.appendMessage(this.whoami, message).then(function() { edit.value = ''; self.change(); self.notify(undefined); self.requestUpdate(); }); } catch (error) { alert(error.message); } } discard() { let edit = this.renderRoot.getElementById('edit'); edit.value = ''; this.change(); let preview = this.renderRoot.getElementById('preview'); preview.innerHTML = ''; this.notify(undefined); } attach() { let self = this; let edit = this.renderRoot.getElementById('edit'); let input = document.createElement('input'); input.type = 'file'; input.onchange = function(event) { let file = event.target.files[0]; self.add_file(file); }; input.click(); } firstUpdated() { let tribute = new Tribute({ values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})), selectTemplate: function(item) { return `[@${item.original.key}](${item.original.value})`; }, }); tribute.attach(this.renderRoot.getElementById('edit')); } updated() { super.updated(); let edit = this.renderRoot.getElementById('edit'); if (this.last_updated_text !== edit.value) { let preview = this.renderRoot.getElementById('preview'); preview.innerHTML = this.process_text(edit.value); this.last_updated_text = edit.value; } let encrypt = this.renderRoot.getElementById('encrypt_to'); if (encrypt) { let tribute = new Tribute({ 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]; this.notify(draft); this.requestUpdate(); } render_mention(mention) { let self = this; return html`
self.remove_mention(mention.link)}>

${mention.name}

${Object.entries(mention) .filter(x => x[0] != 'name') .map(x => html`
${x[0]}: ${x[1]}
`)}
`; } 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`
this.apps = null}>
`; } } render_attach_app_button() { let self = this; async function attach_app() { self.apps = await tfrpc.rpc.apps(); } if (!this.apps) { return html``; } else { return html` this.apps = null}>`; } } 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`
self.set_content_warning(undefined)} checked>
`; } else { return html` self.set_content_warning('')}> `; } } get_draft() { return this.drafts[this.branch || ''] || {}; } 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`
this.set_encrypt(undefined)}>
`; } set_encrypt(encrypt) { let draft = this.get_draft(); draft.encrypt_to = encrypt; this.notify(draft); this.requestUpdate(); } render() { let self = this; let draft = self.get_draft(); let content_warning = draft.content_warning !== undefined ? html`
${draft.content_warning}
` : undefined; let encrypt = draft.encrypt_to !== undefined ? undefined : html` this.set_encrypt([])}>`; let result = html` ${this.render_encrypt()}
${content_warning}
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))} ${this.render_content_warning()} ${this.render_attach_app()} ${this.render_attach_app_button()} ${encrypt} `; return result; } } customElements.define('tf-compose', TfComposeElement);