2023-01-20 19:16:18 -05:00
|
|
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
2022-09-06 19:26:43 -04:00
|
|
|
import * as tfutils from './tf-utils.js';
|
|
|
|
import * as tfrpc from '/static/tfrpc.js';
|
2022-10-15 14:22:13 -04:00
|
|
|
import {styles} from './tf-styles.js';
|
|
|
|
import Tribute from './tribute.esm.js';
|
2022-09-06 19:26:43 -04:00
|
|
|
|
|
|
|
class TfComposeElement extends LitElement {
|
|
|
|
static get properties() {
|
|
|
|
return {
|
|
|
|
whoami: {type: String},
|
|
|
|
users: {type: Object},
|
|
|
|
root: {type: String},
|
|
|
|
branch: {type: String},
|
2022-10-16 15:05:22 -04:00
|
|
|
mentions: {type: Object},
|
2022-10-18 19:00:57 -04:00
|
|
|
apps: {type: Object},
|
2023-01-20 19:16:18 -05:00
|
|
|
drafts: {type: Object},
|
2022-09-06 19:26:43 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-15 14:22:13 -04:00
|
|
|
static styles = styles;
|
|
|
|
|
2022-09-06 19:26:43 -04:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.users = {};
|
|
|
|
this.root = undefined;
|
|
|
|
this.branch = undefined;
|
2022-10-16 15:05:22 -04:00
|
|
|
this.mentions = {};
|
2022-10-18 19:00:57 -04:00
|
|
|
this.apps = undefined;
|
2023-01-20 19:16:18 -05:00
|
|
|
this.drafts = {};
|
2022-09-06 19:26:43 -04:00
|
|
|
}
|
|
|
|
|
2023-01-20 19:16:18 -05:00
|
|
|
process_text(text) {
|
|
|
|
if (!text) {
|
|
|
|
return '';
|
|
|
|
}
|
2022-10-16 15:05:22 -04:00
|
|
|
/* 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);
|
|
|
|
}
|
2023-01-20 19:16:18 -05:00
|
|
|
return tfutils.markdown(text);
|
|
|
|
}
|
2022-10-16 15:05:22 -04:00
|
|
|
|
2023-01-20 20:39:00 -05:00
|
|
|
input(event) {
|
2023-01-20 19:16:18 -05:00
|
|
|
let edit = this.renderRoot.getElementById('edit');
|
|
|
|
let preview = this.renderRoot.getElementById('preview');
|
|
|
|
preview.innerHTML = this.process_text(edit.value);
|
2023-01-20 20:39:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
change(event) {
|
|
|
|
let edit = this.renderRoot.getElementById('edit');
|
2023-01-21 14:12:55 -05:00
|
|
|
this.dispatchEvent(new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.branch, draft: edit.value}}));
|
2022-10-16 15:05:22 -04:00
|
|
|
}
|
|
|
|
|
2023-01-14 17:27:35 -05:00
|
|
|
convert_to_format(buffer, type, mime_type) {
|
2022-10-16 16:31:32 -04:00
|
|
|
return new Promise(function(resolve, reject) {
|
|
|
|
let img = new Image();
|
|
|
|
img.onload = function() {
|
|
|
|
let canvas = document.createElement('canvas');
|
2022-12-09 19:35:53 -05:00
|
|
|
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;
|
2022-10-16 16:31:32 -04:00
|
|
|
let context = canvas.getContext('2d');
|
2022-12-09 19:35:53 -05:00
|
|
|
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
2023-01-14 17:27:35 -05:00
|
|
|
let data_url = canvas.toDataURL(mime_type);
|
2022-10-16 16:31:32 -04:00
|
|
|
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;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-11-29 21:37:27 -05:00
|
|
|
async add_file(file) {
|
|
|
|
try {
|
|
|
|
let self = this;
|
|
|
|
let buffer = await file.arrayBuffer();
|
|
|
|
let type = file.type;
|
|
|
|
if (type.startsWith('image/')) {
|
2023-01-14 17:27:35 -05:00
|
|
|
let best_buffer;
|
|
|
|
let best_type;
|
2023-01-17 18:10:17 -05:00
|
|
|
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
|
2023-01-14 17:27:35 -05:00
|
|
|
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;
|
2022-11-29 21:37:27 -05:00
|
|
|
} else {
|
|
|
|
buffer = Array.from(new Uint8Array(buffer));
|
|
|
|
}
|
|
|
|
let id = await tfrpc.rpc.store_blob(buffer);
|
|
|
|
let name = type.split('/')[0] + ':' + file.name;
|
2022-10-16 15:05:22 -04:00
|
|
|
self.mentions[id] = {
|
|
|
|
link: id,
|
2022-11-29 21:37:27 -05:00
|
|
|
name: name,
|
|
|
|
type: type,
|
|
|
|
size: buffer.length ?? buffer.byteLength,
|
2022-10-16 15:05:22 -04:00
|
|
|
};
|
|
|
|
self.mentions = Object.assign({}, self.mentions);
|
|
|
|
let edit = self.renderRoot.getElementById('edit');
|
2022-11-29 21:37:27 -05:00
|
|
|
edit.value += `\n![${name}](${id})`;
|
2023-01-20 20:39:00 -05:00
|
|
|
self.change();
|
2022-11-29 21:37:27 -05:00
|
|
|
} catch(e) {
|
2022-10-16 16:31:32 -04:00
|
|
|
alert(e?.message);
|
2022-11-29 21:37:27 -05:00
|
|
|
}
|
2022-09-06 19:26:43 -04:00
|
|
|
}
|
|
|
|
|
2022-10-04 21:40:21 -04:00
|
|
|
paste(event) {
|
|
|
|
let self = this;
|
2022-10-16 16:31:32 -04:00
|
|
|
for (let item of event.clipboardData.items) {
|
2022-10-04 21:40:21 -04:00
|
|
|
if (item.type?.startsWith('image/')) {
|
|
|
|
let file = item.getAsFile();
|
|
|
|
if (!file) {
|
|
|
|
continue;
|
|
|
|
}
|
2022-10-16 15:05:22 -04:00
|
|
|
self.add_file(file);
|
2022-10-04 21:40:21 -04:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-06 19:26:43 -04:00
|
|
|
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;
|
|
|
|
}
|
2022-10-16 15:05:22 -04:00
|
|
|
if (Object.values(this.mentions).length) {
|
|
|
|
message.mentions = Object.values(this.mentions);
|
2022-10-15 15:02:09 -04:00
|
|
|
}
|
2022-09-06 19:26:43 -04:00
|
|
|
console.log('Would post:', message);
|
|
|
|
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
|
|
|
edit.value = '';
|
2022-11-08 20:47:47 -05:00
|
|
|
self.mentions = {};
|
2023-01-20 20:39:00 -05:00
|
|
|
self.change();
|
2023-01-20 19:16:18 -05:00
|
|
|
this.dispatchEvent(new CustomEvent('tf-draft', {detail: {id: this.branch, discard: undefined}}));
|
2022-09-06 19:26:43 -04:00
|
|
|
}).catch(function(error) {
|
|
|
|
alert(error.message);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
discard() {
|
|
|
|
let edit = this.renderRoot.getElementById('edit');
|
|
|
|
edit.value = '';
|
2023-01-20 20:39:00 -05:00
|
|
|
this.change();
|
2023-01-20 19:16:18 -05:00
|
|
|
this.dispatchEvent(new CustomEvent('tf-draft', {bubble: true, composed: true, detail: {id: this.branch, discard: undefined}}));
|
2022-09-06 19:26:43 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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];
|
2022-10-16 15:05:22 -04:00
|
|
|
self.add_file(file);
|
2022-09-06 19:26:43 -04:00
|
|
|
};
|
|
|
|
input.click();
|
|
|
|
}
|
|
|
|
|
2022-10-15 14:22:13 -04:00
|
|
|
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'));
|
|
|
|
}
|
|
|
|
|
2022-10-16 15:05:22 -04:00
|
|
|
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>`;
|
|
|
|
}
|
|
|
|
|
2022-10-18 19:00:57 -04:00
|
|
|
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>`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-06 19:26:43 -04:00
|
|
|
render() {
|
2022-10-16 15:05:22 -04:00
|
|
|
let self = this;
|
2022-10-15 14:22:13 -04:00
|
|
|
let result = html`
|
2022-09-06 19:26:43 -04:00
|
|
|
<div style="display: flex; flex-direction: row; width: 100%">
|
2023-01-21 14:12:55 -05:00
|
|
|
<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${this.drafts[this.branch || '']}</textarea>
|
2022-09-06 19:26:43 -04:00
|
|
|
<div id="preview" style="flex: 1 0 50%"></div>
|
|
|
|
</div>
|
2022-10-16 15:05:22 -04:00
|
|
|
${Object.values(this.mentions).map(x => self.render_mention(x))}
|
2022-10-18 19:00:57 -04:00
|
|
|
${this.render_attach_app()}
|
2022-09-06 19:26:43 -04:00
|
|
|
<input type="button" value="Submit" @click=${this.submit}></input>
|
|
|
|
<input type="button" value="Attach" @click=${this.attach}></input>
|
2022-10-18 19:00:57 -04:00
|
|
|
${this.render_attach_app_button()}
|
2022-09-06 19:26:43 -04:00
|
|
|
<input type="button" value="Discard" @click=${this.discard}></input>
|
|
|
|
`;
|
2022-10-15 14:22:13 -04:00
|
|
|
return result;
|
2022-09-06 19:26:43 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
customElements.define('tf-compose', TfComposeElement);
|