import {LitElement, html, unsafeHTML} from './lit-all.min.js'; import * as tfrpc from '/static/tfrpc.js'; import * as tfutils from './tf-utils.js'; import * as emojis from './emojis.js'; import {styles} from './tf-styles.js'; class TfMessageElement extends LitElement { static get properties() { return { whoami: {type: String}, message: {type: Object}, users: {type: Object}, reply: {type: Boolean}, raw: {type: Boolean}, collapsed: {type: Boolean}, } } static styles = styles; constructor() { super(); let self = this; this.whoami = null; this.message = {}; this.users = {}; this.reply = false; this.raw = false; this.collapsed = false; } show_reply() { this.reply = true; } render_votes() { function normalize_expression(expression) { if (expression === 'Like' || !expression) { return '👍'; } else if (expression === 'Unlike') { return '👎'; } else if (expression === 'heart') { return '❤️'; } else { return expression; } } return html`<div>${(this.message.votes || []).map( vote => html` <span title="${this.users[vote.author]?.name ?? vote.author} ${new Date(vote.timestamp)}"> ${normalize_expression(vote.content.vote.expression)} </span> `)}</div>`; } render_raw() { let raw = { id: this.message?.id, previous: this.message?.previous, author: this.message?.author, sequence: this.message?.sequence, timestamp: this.message?.timestamp, hash: this.message?.hash, content: this.message?.content, signature: this.message?.signature, } return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>` } vote(emoji) { let reaction = emoji.emoji; let message = this.message.id; if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) { tfrpc.rpc.appendMessage( this.whoami, { type: 'vote', vote: { link: message, value: 1, expression: reaction, }, }).catch(function(error) { alert(error?.message); }); } } react(event) { emojis.picker(x => this.vote(x)); } show_image(link) { let div = document.createElement('div'); div.style.left = 0; div.style.top = 0; div.style.width = '100%'; div.style.height = '100%'; div.style.position = 'fixed'; div.style.background = '#000'; div.style.zIndex = 100; div.style.display = 'grid'; let img = document.createElement('img'); img.src = link; img.style.maxWidth = '100%'; img.style.maxHeight = '100%'; img.style.display = 'block'; img.style.margin = 'auto'; img.style.objectFit = 'contain'; img.style.width = '100%'; div.appendChild(img); function image_close(event) { document.body.removeChild(div); window.removeEventListener('keydown', image_close); } div.onclick = image_close; window.addEventListener('keydown', image_close); document.body.appendChild(div); } body_click(event) { if (event.srcElement.tagName == 'IMG') { this.show_image(event.srcElement.src); } } render_mention(mention) { if (!mention?.link || typeof(mention.link) != 'string') { return html` <pre>${JSON.stringify(mention)}</pre>`; } else if (mention?.link?.startsWith('&') && mention?.type?.startsWith('image/')) { return html` <img src=${'/' + mention.link + '/view'} style="max-width: 128px; max-height: 128px" title=${mention.name} @click=${() => this.show_image('/' + mention.link + '/view')}> `; } else if (mention.link?.startsWith('&') && mention.name?.startsWith('audio:')) { return html` <audio controls style="height: 32px"> <source src=${'/' + mention.link + '/view'}></source> </audio> `; } else if (mention.link?.startsWith('&') && mention?.type === 'application/tildefriends') { return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`; } else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) { return html` <a href=${'#' + encodeURIComponent(mention.link)}>${mention.name}</a>`; } else if (mention.link?.startsWith('#')) { return html` <a href=${'#q=' + encodeURIComponent(mention.link)}>${mention.link}</a>`; } else if (Object.keys(mention).length == 2 && mention.link && mention.name) { return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`; } else { return html` <pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>`; } } render_mentions() { let mentions = this.message?.content?.mentions || []; mentions = mentions.filter(x => x.name?.startsWith('audio:') || this.message?.content?.text?.indexOf(x.link) === -1); if (mentions.length) { let self = this; return html` <fieldset style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black"> <legend>Mentions</legend> ${mentions.map(x => self.render_mention(x))} </fieldset> `; } } render_children() { let self = this; if (this.collapsed && this.message.child_messages?.length) { return html`<input type="button" value=${this.message.child_messages?.length + ' More'} @click=${() => self.collapsed = false}></input>`; } else { return html`${(this.message.child_messages || []).map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users}></tf-message>`)}`; } } render() { let content = this.message?.content; let self = this; let raw_button = this.raw ? html`<input type="button" value="Message" @click=${() => self.raw = false}></input>` : html`<input type="button" value="Raw" @click=${() => self.raw = true}></input>`; function small_frame(inner) { return html` <div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block"> <tf-user id=${self.message.author} .users=${self.users}></tf-user> <span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span> ${raw_button} ${self.raw ? self.render_raw() : inner} ${self.render_votes()} </div> ` } if (this.message.placeholder) { return html` <div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"> <a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder) <div>${this.render_votes()}</div> ${(this.message.child_messages || []).map(x => html` <tf-message .message=${x} whoami=${this.whoami} .users=${this.users} collapsed=true></tf-message> `)} </div>`; } else if (typeof(content?.type === 'string')) { if (content.type == 'about') { return small_frame(html` <div style="font-weight: bold">Updated profile:</div> <pre style="white-space: pre-wrap; overflow-wrap: anywhere">${JSON.stringify(content, null, 2)}</pre> `); } else if (content.type == 'contact') { return small_frame(html` <div> is ${ content.blocking === true ? 'blocking' : content.blocking === false ? 'no longer blocking' : content.following === true ? 'following' : content.following === false ? 'no longer following' : '?' } <tf-user id=${this.message.content.contact} .users=${this.users}></tf-user> </div> `); } else if (content.type == 'post') { let reply = this.reply ? html` <tf-compose ?enabled=${this.reply} whoami=${this.whoami} .users=${this.users} root=${this.message.content.root || this.message.id} branch=${this.message.id} @tf-discard=${() => this.reply = false}></tf-compose> ` : html` <input type="button" value="Reply" @click=${this.show_reply}></input> `; let self = this; let body = this.raw ? this.render_raw() : unsafeHTML(tfutils.markdown(content.text)); return html` <style> code { white-space: pre-wrap; overflow-wrap: break-word; } div { overflow-wrap: anywhere; } img { max-width: 100%; height: auto; display: block; } </style> <div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px"> <div style="display: flex; flex-direction: row"> <tf-user id=${this.message.author} .users=${this.users}></tf-user> <span style="flex: 1"></span> <span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span> <span>${raw_button}</span> </div> <div @click=${this.body_click}>${body}</div> ${this.render_mentions()} ${this.render_votes()} <div> ${reply} <input type="button" value="React" @click=${this.react}></input> </div> ${this.render_children()} </div> `; } else if (content.type === 'pub') { return small_frame(html` <span> <div> 🍻 <tf-user .users=${this.users} id=${content.address.key}></tf-user> </div> <pre>${content.address.host}:${content.address.port}</pre> </span>`); } else if (typeof(this.message.content) == 'string') { return small_frame(html`<span>🔒</span>`); } else { return small_frame(html`<div><b>type</b>: ${content.type}</div>`); } } else { return small_frame(this.render_raw()); } } } customElements.define('tf-message', TfMessageElement);