import {LitElement, html, unsafeHTML} from './lit-all.min.js'; import * as tfrpc from '/static/tfrpc.js'; import * as tfutils from './tf-utils.js'; const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256'; class TfIdPickerElement extends LitElement { static get properties() { return { ids: {type: Array}, selected: {type: String}, }; } constructor() { super(); this.load(); } async load() { this.selected = await tfrpc.rpc.localStorageGet('whoami'); this.ids = (await tfrpc.rpc.getIdentities()) || []; } changed(event) { this.selected = event.srcElement.value; tfrpc.rpc.localStorageSet('whoami', this.selected); } render() { if (this.ids) { return html` <select @change=${this.changed} style="max-width: 100%"> ${(this.ids).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)} </select> `; } else { return html`<div>Loading...</div>`; } } } customElements.define('tf-id-picker', TfIdPickerElement); class TfComposeElement extends LitElement { static get properties() { return { value: {type: String}, }; } input() { let input = this.renderRoot.getElementById('input'); let preview = this.renderRoot.getElementById('preview'); if (input && preview) { preview.innerHTML = tfutils.markdown(input.value); } } submit() { this.dispatchEvent(new CustomEvent('tf-submit', { bubbles: true, composed: true, detail: { value: this.renderRoot.getElementById('input').value, }, })); this.renderRoot.getElementById('input').value = ''; this.input(); } render() { return html` <div style="display: flex; flex-direction: row"> <textarea id="input" @input=${this.input} style="flex: 1 1">${this.value}</textarea> <div id="preview" style="flex: 1 1"></div> </div> <input type="submit" value="Submit" @click=${this.submit}></input> `; } } customElements.define('tf-compose', TfComposeElement); class TfIssuesAppElement extends LitElement { static get properties() { return { issues: {type: Array}, selected: {type: Object}, }; } constructor() { super(); this.issues = []; this.load(); } async load() { let issues = {}; let messages = await tfrpc.rpc.query(` WITH issues AS (SELECT messages.* FROM messages_refs JOIN messages ON messages.id = messages_refs.message WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'), edits AS (SELECT messages.* FROM issues JOIN messages_refs ON issues.id = messages_refs.ref JOIN messages ON messages.id = messages_refs.message WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post')) SELECT * FROM issues UNION SELECT * FROM edits ORDER BY timestamp `, [k_project]); for (let message of messages) { let content = JSON.parse(message.content); switch (content.type) { case 'issue': issues[message.id] = { id: message.id, author: message.author, text: content.text, updates: [], created: message.timestamp, open: true, }; break; case 'issue-edit': case 'post': for (let issue of (content.issues || [])) { if (issues[issue.link]) { if (issue.open !== undefined) { issues[issue.link].open = issue.open; message.open = issue.open; } issues[issue.link].updates.push(message); issues[issue.link].updated = message.timestamp; } } break; } } this.issues = Object.values(issues).sort((x, y) => y.created - x.created); if (this.selected) { for (let issue of this.issues) { if (issue.id == this.selected.id) { this.selected = issue; } } } } render_issue_table_row(issue) { return html` <tr> <td>${issue.open ? 'open' : 'closed'}</td> <td style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis">${issue.author}</td> <td style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer" @click=${() => this.selected = issue}> ${issue.text.split('\n')?.[0]} </td> <td>${new Date(issue.updated ?? issue.created).toLocaleDateString()}</td> </tr> `; } render_update(update) { let content = JSON.parse(update.content); let message; if (content.text) { message = unsafeHTML(tfutils.markdown(content.text)); } return html` <div style="border-left: 2px solid #fff; padding-left: 8px"> <div>${new Date(update.timestamp).toLocaleString()}</div> <div>${update.author}</div> <div>${message}</div> <div>${update.open !== undefined ? (update.open ? 'issue opened' : 'issue closed') : undefined}</div> </div> `; } async set_open(id, open) { if (confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)) { let whoami = this.shadowRoot.getElementById('picker').selected; await tfrpc.rpc.appendMessage(whoami, { type: 'issue-edit', issues: [ { link: id, open: open, }, ], }); await this.load(); } } async create_issue(event) { let whoami = this.shadowRoot.getElementById('picker').selected; await tfrpc.rpc.appendMessage(whoami, { type: 'issue', project: k_project, text: event.detail.value, }); await this.load(); } async reply_to_issue(event) { let whoami = this.shadowRoot.getElementById('picker').selected; await tfrpc.rpc.appendMessage(whoami, { type: 'post', text: event.detail.value, root: this.selected.id, branch: this.selected.updates.length ? this.selected.updates[this.selected.updates.length - 1].id : this.selected.id, issues: [ { link: this.selected.id, }, ], }); await this.load(); } render() { let header = html` <h1>Tilde Friends Issues</h1> <tf-id-picker id="picker"></tf-id-picker> `; if (this.selected) { return html` ${header} <div> <input type="button" value="Back" @click=${() => this.selected = undefined}></input> ${this.selected.open ? html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>` : html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`} </div> <div>${new Date(this.selected.created).toLocaleString()}</div> <div>${this.selected.author}</div> <div>${this.selected.id}</div> <div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div> ${this.selected.updates.map(x => this.render_update(x))} <tf-compose @tf-submit=${this.reply_to_issue}></tf-compose> `; } else { return html` ${header} <h2>New Issue</h2> <tf-compose @tf-submit=${this.create_issue}></tf-compose> <table> <tr> <th>Status</th> <th>Author</th> <th>Title</th> <th>Date</th> </tr> ${this.issues.map(x => this.render_issue_table_row(x))} </table> `; } } } customElements.define('tf-issues-app', TfIssuesAppElement);