tildefriends/apps/cory/ssblit/tf-compose.js

256 lines
6.7 KiB
JavaScript

import {LitElement, html} 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},
}
}
static styles = styles;
constructor() {
super();
this.users = {};
this.root = undefined;
this.branch = undefined;
this.mentions = {};
this.apps = undefined;
}
changed(event) {
let edit = this.renderRoot.getElementById('edit');
let preview = this.renderRoot.getElementById('preview');
let text = edit.value;
/* 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);
}
preview.innerHTML = tfutils.markdown(text);
}
convert_to_webp(buffer, type) {
return new Promise(function(resolve, reject) {
let img = new Image();
img.onload = function() {
let canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
let context = canvas.getContext('2d');
context.drawImage(img, 0, 0);
let data_url = canvas.toDataURL('image/webp');
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;
});
}
add_file(file) {
let self = this;
file.arrayBuffer().then(function(buffer) {
return self.convert_to_webp(buffer, file.type);
}).then(function(bin) {
return Promise.all([tfrpc.rpc.store_blob(bin), bin]);
}).then(function([id, bin]) {
self.mentions[id] = {
link: id,
name: file.name,
type: 'image/webp',
size: bin.length,
};
self.mentions = Object.assign({}, self.mentions);
let edit = self.renderRoot.getElementById('edit');
edit.value += `\n![${file.name}](${id})`;
self.changed();
}).catch(function(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.changed();
}).catch(function(error) {
alert(error.message);
});
}
discard() {
let edit = this.renderRoot.getElementById('edit');
edit.value = '';
this.changed();
this.dispatchEvent(new CustomEvent('tf-discard'));
}
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`
<div>
<pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>
<input type="button" value="x" @click=${() => self.remove_mention(mention.link)}></input>
</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],
};
}
}
this.mentions = Object.assign(this.mentions || {}, mentions);
this.apps = null;
}
if (this.apps) {
return html`
<div>
<select id="select">
${Object.keys(self.apps).map(app => html`<option value=${app}>${app}</option>`)}
</select>
<input type="button" value="Attach" @click=${attach_selected_app}></input>
<input type="button" value="Cancel" @click=${() => this.apps = null}></input>
</div>
`;
}
}
render_attach_app_button() {
async function attach_app() {
this.apps = await tfrpc.rpc.apps();
}
if (!this.apps) {
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`
} else {
return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`
}
}
render() {
let self = this;
let result = html`
<div style="display: flex; flex-direction: row; width: 100%">
<textarea id="edit" @input=${this.changed} @paste=${this.paste} style="flex: 1 0 50%"></textarea>
<div id="preview" style="flex: 1 0 50%"></div>
</div>
${Object.values(this.mentions).map(x => self.render_mention(x))}
${this.render_attach_app()}
<input type="button" value="Submit" @click=${this.submit}></input>
<input type="button" value="Attach" @click=${this.attach}></input>
${this.render_attach_app_button()}
<input type="button" value="Discard" @click=${this.discard}></input>
`;
return result;
}
}
customElements.define('tf-compose', TfComposeElement);