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}, mentions: {type: Object}, apps: {type: Object}, drafts: {type: Object}, } } static styles = styles; constructor() { super(); this.users = {}; this.root = undefined; this.branch = undefined; this.mentions = {}; this.apps = undefined; this.drafts = {}; } process_text(text) { if (!text) { return ''; } /* Update mentions. */ 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 (!this.mentions[link]) { this.mentions[link] = { link: link, } } this.mentions[link].name = name.startsWith('@') ? name.substring(1) : name; this.mentions = Object.assign({}, this.mentions); } 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); } change(event) { let edit = this.renderRoot.getElementById('edit'); this.dispatchEvent(new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.branch, draft: edit.value}})); } 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 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; self.mentions[id] = { link: id, name: name, type: type, size: buffer.length ?? buffer.byteLength, }; self.mentions = Object.assign({}, self.mentions); let edit = self.renderRoot.getElementById('edit'); edit.value += `\n![${name}](${id})`; self.change(); } 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; } } } submit() { let self = this; 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(this.mentions).length) { message.mentions = Object.values(this.mentions); } console.log('Would post:', message); tfrpc.rpc.appendMessage(this.whoami, message).then(function() { edit.value = ''; self.mentions = {}; self.change(); this.dispatchEvent(new CustomEvent('tf-draft', {detail: {id: this.branch, discard: undefined}})); }).catch(function(error) { alert(error.message); }); } discard() { let edit = this.renderRoot.getElementById('edit'); edit.value = ''; this.change(); this.dispatchEvent(new CustomEvent('tf-draft', {bubble: true, composed: true, detail: {id: this.branch, discard: 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')); } remove_mention(id) { delete this.mentions[id]; this.mentions = Object.assign({}, this.mentions); } render_mention(mention) { let self = this; return html`
${JSON.stringify(mention, null, 2)}
self.remove_mention(mention.link)}>
`; } 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], }; } } this.mentions = Object.assign(this.mentions || {}, mentions); this.apps = null; } if (this.apps) { return html`
this.apps = null}>
`; } } render_attach_app_button() { async function attach_app() { this.apps = await tfrpc.rpc.apps(); } if (!this.apps) { return html`` } else { return html` this.apps = null}>` } } render() { let self = this; let result = html`
${Object.values(this.mentions).map(x => self.render_mention(x))} ${this.render_attach_app()} ${this.render_attach_app_button()} `; return result; } } customElements.define('tf-compose', TfComposeElement);