All checks were successful
		
		
	
	Build Tilde Friends / Build-All (push) Successful in 18m1s
				
			
		
			
				
	
	
		
			1112 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1112 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import {
 | 
						|
	LitElement,
 | 
						|
	css,
 | 
						|
	html,
 | 
						|
	repeat,
 | 
						|
	render,
 | 
						|
	unsafeCSS,
 | 
						|
	unsafeHTML,
 | 
						|
	until,
 | 
						|
} 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, generate_theme} from './tf-styles.js';
 | 
						|
 | 
						|
class TfMessageElement extends LitElement {
 | 
						|
	static get properties() {
 | 
						|
		return {
 | 
						|
			whoami: {type: String},
 | 
						|
			message: {type: Object},
 | 
						|
			users: {type: Object},
 | 
						|
			drafts: {type: Object},
 | 
						|
			format: {type: String},
 | 
						|
			blog_data: {type: String},
 | 
						|
			expanded: {type: Object},
 | 
						|
			channel: {type: String},
 | 
						|
			channel_unread: {type: Number},
 | 
						|
			recent_reactions: {type: Array},
 | 
						|
			depth: {type: Number},
 | 
						|
		};
 | 
						|
	}
 | 
						|
 | 
						|
	static styles = styles;
 | 
						|
 | 
						|
	constructor() {
 | 
						|
		super();
 | 
						|
		let self = this;
 | 
						|
		this.whoami = null;
 | 
						|
		this.message = {};
 | 
						|
		this.users = {};
 | 
						|
		this.drafts = {};
 | 
						|
		this.format = 'message';
 | 
						|
		this.expanded = {};
 | 
						|
		this.channel_unread = -1;
 | 
						|
		this.recent_reactions = [];
 | 
						|
		this.depth = 0;
 | 
						|
	}
 | 
						|
 | 
						|
	connectedCallback() {
 | 
						|
		super.connectedCallback();
 | 
						|
		this._click_callback = this.document_click.bind(this);
 | 
						|
		this._blob_stored = this.blob_stored.bind(this);
 | 
						|
		document.body.addEventListener('mouseup', this._click_callback);
 | 
						|
		window.addEventListener('blob-stored', this._blob_stored);
 | 
						|
	}
 | 
						|
 | 
						|
	disconnectedCallback() {
 | 
						|
		super.disconnectedCallback();
 | 
						|
		window.removeEventListener('blob-stored', this._blob_stored);
 | 
						|
		document.body.removeEventListener('mouseup', this._click_callback);
 | 
						|
	}
 | 
						|
 | 
						|
	document_click(event) {
 | 
						|
		let content = this.renderRoot.querySelector('.w3-dropdown-content');
 | 
						|
		let target = event.target;
 | 
						|
		if (content && !content.contains(target)) {
 | 
						|
			content.classList.remove('w3-show');
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	blob_stored(event) {
 | 
						|
		let search = `/${event.detail.id}/view`;
 | 
						|
		for (let img of this.shadowRoot.querySelectorAll('img')) {
 | 
						|
			if (img.src.indexOf(search) != -1) {
 | 
						|
				let src = img.src.split('?')[0];
 | 
						|
				img.src = `${src}?${new Date().valueOf()}`;
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	show_reply() {
 | 
						|
		let event = new CustomEvent('tf-draft', {
 | 
						|
			bubbles: true,
 | 
						|
			composed: true,
 | 
						|
			detail: {
 | 
						|
				id: this.message?.id,
 | 
						|
				draft: {
 | 
						|
					encrypt_to: this.message?.decrypted?.recps,
 | 
						|
				},
 | 
						|
			},
 | 
						|
		});
 | 
						|
		this.dispatchEvent(event);
 | 
						|
	}
 | 
						|
 | 
						|
	discard_reply() {
 | 
						|
		this.dispatchEvent(
 | 
						|
			new CustomEvent('tf-draft', {
 | 
						|
				bubbles: true,
 | 
						|
				composed: true,
 | 
						|
				detail: {id: this.id, draft: undefined},
 | 
						|
			})
 | 
						|
		);
 | 
						|
	}
 | 
						|
 | 
						|
	show_reactions() {
 | 
						|
		let modal = document.getElementById('reactions_modal');
 | 
						|
		modal.users = this.users;
 | 
						|
		modal.votes = this.message?.votes || [];
 | 
						|
	}
 | 
						|
 | 
						|
	render_votes() {
 | 
						|
		function normalize_expression(expression) {
 | 
						|
			if (
 | 
						|
				expression === 'Unlike' ||
 | 
						|
				expression === 'unlike' ||
 | 
						|
				expression == 'undig'
 | 
						|
			) {
 | 
						|
				return '👎';
 | 
						|
			} else if (expression === 'heart') {
 | 
						|
				return '❤️';
 | 
						|
			} else if (
 | 
						|
				(expression ?? '').split('').every((x) => x.charCodeAt(0) < 256)
 | 
						|
			) {
 | 
						|
				return '👍';
 | 
						|
			} else {
 | 
						|
				return expression;
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if (this.message?.votes?.length) {
 | 
						|
			return html` <footer class="w3-container">
 | 
						|
				<div
 | 
						|
					class="w3-button w3-bar"
 | 
						|
					style="padding: 0"
 | 
						|
					@click=${this.show_reactions}
 | 
						|
				>
 | 
						|
					${(this.message.votes || []).map(
 | 
						|
						(vote) => html`
 | 
						|
							<span
 | 
						|
								class="w3-bar-item w3-padding-small"
 | 
						|
								title="${this.users[vote.author]?.name ??
 | 
						|
								vote.author} ${new Date(vote.timestamp)}"
 | 
						|
							>
 | 
						|
								${normalize_expression(vote.content.vote.expression)}
 | 
						|
							</span>
 | 
						|
						`
 | 
						|
					)}
 | 
						|
				</div>
 | 
						|
			</footer>`;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	render_json(value) {
 | 
						|
		let json = JSON.stringify(value, null, 2);
 | 
						|
		return html`
 | 
						|
			<pre style="white-space: pre-wrap; overflow-wrap: anywhere">${json}</pre>
 | 
						|
		`;
 | 
						|
	}
 | 
						|
 | 
						|
	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 this.render_json(raw);
 | 
						|
	}
 | 
						|
 | 
						|
	vote(emoji) {
 | 
						|
		let reaction = emoji;
 | 
						|
		let message = this.message.id;
 | 
						|
		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),
 | 
						|
			null,
 | 
						|
			this.whoami,
 | 
						|
			this.recent_reactions
 | 
						|
		);
 | 
						|
	}
 | 
						|
 | 
						|
	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 = '100vw';
 | 
						|
		img.style.maxHeight = '100vh';
 | 
						|
		img.style.display = 'block';
 | 
						|
		img.style.margin = 'auto';
 | 
						|
		img.style.objectFit = 'contain';
 | 
						|
		img.style.width = '100vw';
 | 
						|
		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);
 | 
						|
		} else if (
 | 
						|
			event.srcElement.tagName == 'DIV' &&
 | 
						|
			event.srcElement.classList.contains('img_caption')
 | 
						|
		) {
 | 
						|
			let next = event.srcElement.nextSibling;
 | 
						|
			if (next.style.display != 'none') {
 | 
						|
				next.style.display = 'none';
 | 
						|
			} else {
 | 
						|
				next.style.display = 'block';
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	render_mention(mention) {
 | 
						|
		if (!mention?.link || typeof mention.link != 'string') {
 | 
						|
			return this.render_json(mention);
 | 
						|
		} 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.name?.startsWith('video:')
 | 
						|
		) {
 | 
						|
			return html`
 | 
						|
				<video controls style="max-height: 240px; max-width: 128px">
 | 
						|
					<source src=${'/' + mention.link + '/view'}></source>
 | 
						|
				</video>
 | 
						|
			`;
 | 
						|
		} 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=${'#' + 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 this.render_json(mention);
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	render_mentions() {
 | 
						|
		let mentions = this.message?.content?.mentions || [];
 | 
						|
		mentions = mentions.filter(
 | 
						|
			(x) =>
 | 
						|
				this.message?.content?.text?.indexOf(
 | 
						|
					typeof x === 'string' ? x : x.link
 | 
						|
				) === -1
 | 
						|
		);
 | 
						|
		if (mentions.length) {
 | 
						|
			let self = this;
 | 
						|
			return html`
 | 
						|
				<fieldset style="padding: 0.5em; border: 1px solid black">
 | 
						|
					<legend>Mentions</legend>
 | 
						|
					${mentions.map((x) => self.render_mention(x))}
 | 
						|
				</fieldset>
 | 
						|
			`;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	total_child_messages(message) {
 | 
						|
		if (!message.child_messages) {
 | 
						|
			return 0;
 | 
						|
		}
 | 
						|
		let total = message.child_messages.length;
 | 
						|
		for (let m of message.child_messages) {
 | 
						|
			total += this.total_child_messages(m);
 | 
						|
		}
 | 
						|
		return total;
 | 
						|
	}
 | 
						|
 | 
						|
	expanded_key() {
 | 
						|
		return (
 | 
						|
			this.message?.id || this.message?.messages?.map((x) => x.id).join(':')
 | 
						|
		);
 | 
						|
	}
 | 
						|
 | 
						|
	set_expanded(expanded, tag) {
 | 
						|
		let key = this.expanded_key();
 | 
						|
		this.dispatchEvent(
 | 
						|
			new CustomEvent('tf-expand', {
 | 
						|
				bubbles: true,
 | 
						|
				composed: true,
 | 
						|
				detail: {id: key + (tag || ''), expanded: expanded},
 | 
						|
			})
 | 
						|
		);
 | 
						|
	}
 | 
						|
 | 
						|
	toggle_expanded(tag) {
 | 
						|
		let key = this.expanded_key();
 | 
						|
		this.set_expanded(!this.expanded[key + (tag || '')], tag);
 | 
						|
	}
 | 
						|
 | 
						|
	is_expanded(tag) {
 | 
						|
		let key = this.expanded_key();
 | 
						|
		return this.expanded[key + (tag || '')];
 | 
						|
	}
 | 
						|
 | 
						|
	render_children() {
 | 
						|
		let self = this;
 | 
						|
		if (this.message.child_messages?.length) {
 | 
						|
			if (!this.expanded[this.expanded_key()]) {
 | 
						|
				return html`
 | 
						|
					<button
 | 
						|
						class="w3-button w3-theme-d1 w3-block w3-bar"
 | 
						|
						style="box-sizing: border-box"
 | 
						|
						@click=${() => self.set_expanded(true)}
 | 
						|
					>
 | 
						|
						+ ${this.total_child_messages(this.message) + ' More'}
 | 
						|
					</button>
 | 
						|
				`;
 | 
						|
			} else {
 | 
						|
				return html` <ul class="w3-container w3-margin-bottom w3-ul w3-card-4">
 | 
						|
					${repeat(
 | 
						|
						this.message.child_messages || [],
 | 
						|
						(x) => x.id,
 | 
						|
						(x) =>
 | 
						|
							html`<li style="padding: 0">
 | 
						|
								<tf-message
 | 
						|
									.message=${x}
 | 
						|
									whoami=${this.whoami}
 | 
						|
									.users=${this.users}
 | 
						|
									.drafts=${this.drafts}
 | 
						|
									.expanded=${this.expanded}
 | 
						|
									channel=${this.channel}
 | 
						|
									channel_unread=${this.channel_unread}
 | 
						|
									.recent_reactions=${this.recent_reactions}
 | 
						|
									depth=${this.depth + 1}
 | 
						|
								></tf-message>
 | 
						|
							</li>`
 | 
						|
					)}
 | 
						|
					<li style="padding: 0" class="w3-margin-bottom">
 | 
						|
						<button
 | 
						|
							class="w3-button w3-theme-d1 w3-block w3-bar"
 | 
						|
							style="box-sizing: border-box"
 | 
						|
							@click=${() => self.set_expanded(false)}
 | 
						|
						>
 | 
						|
							Collapse
 | 
						|
						</button>
 | 
						|
					</li>
 | 
						|
				</ul>`;
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			return undefined;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	mark_unread() {
 | 
						|
		this.dispatchEvent(
 | 
						|
			new CustomEvent('channelsetunread', {
 | 
						|
				bubbles: true,
 | 
						|
				composed: true,
 | 
						|
				detail: {
 | 
						|
					channel: this.channel,
 | 
						|
					unread: this.message.rowid,
 | 
						|
				},
 | 
						|
			})
 | 
						|
		);
 | 
						|
	}
 | 
						|
 | 
						|
	render_channels() {
 | 
						|
		let content = this.message?.content;
 | 
						|
		if (this?.messsage?.decrypted?.type == 'post') {
 | 
						|
			content = this.message.decrypted;
 | 
						|
		}
 | 
						|
		let channels = [];
 | 
						|
		if (typeof content.channel === 'string') {
 | 
						|
			channels.push(`#${content.channel}`);
 | 
						|
		}
 | 
						|
		if (Array.isArray(content.mentions)) {
 | 
						|
			for (let mention of content.mentions) {
 | 
						|
				if (typeof mention?.link === 'string' && mention.link.startsWith('#')) {
 | 
						|
					channels.push(mention.link);
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`);
 | 
						|
	}
 | 
						|
 | 
						|
	class_background() {
 | 
						|
		return this.message?.decrypted
 | 
						|
			? 'w3-pale-red'
 | 
						|
			: this.allow_unread() && this.message?.rowid >= this.channel_unread
 | 
						|
				? 'w3-theme-d2'
 | 
						|
				: 'w3-theme-d4';
 | 
						|
	}
 | 
						|
 | 
						|
	get_content() {
 | 
						|
		let content = this.message?.content;
 | 
						|
		if (this.message?.decrypted?.type == 'post') {
 | 
						|
			content = this.message.decrypted;
 | 
						|
		}
 | 
						|
		return content;
 | 
						|
	}
 | 
						|
 | 
						|
	copy_id(event) {
 | 
						|
		navigator.clipboard.writeText(this.message?.id);
 | 
						|
	}
 | 
						|
 | 
						|
	toggle_menu(event) {
 | 
						|
		event.srcElement.parentNode
 | 
						|
			.querySelector('.w3-dropdown-content')
 | 
						|
			.classList.toggle('w3-show');
 | 
						|
	}
 | 
						|
 | 
						|
	render_menu() {
 | 
						|
		let content = this.get_content();
 | 
						|
		let formats = [['message', 'Message']];
 | 
						|
		if (content?.type == 'post' || content?.type == 'blog') {
 | 
						|
			formats.push(['md', 'Markdown']);
 | 
						|
		}
 | 
						|
		if (this.message?.decrypted) {
 | 
						|
			formats.push(['decrypted', 'Decrypted']);
 | 
						|
		}
 | 
						|
		formats.push(['raw', 'Raw']);
 | 
						|
		return html`
 | 
						|
			<div class="w3-bar-item w3-right">
 | 
						|
				<button class="w3-button w3-theme-d1" @click=${this.toggle_menu}>
 | 
						|
					%
 | 
						|
				</button>
 | 
						|
				<div
 | 
						|
					class="w3-dropdown-content w3-bar-block w3-card-4 w3-theme-l1"
 | 
						|
					style="right: 48px"
 | 
						|
				>
 | 
						|
					<a
 | 
						|
						target="_top"
 | 
						|
						class="w3-button w3-bar-item"
 | 
						|
						href=${'#' + encodeURIComponent(this.message?.id)}
 | 
						|
						>View Message</a
 | 
						|
					>
 | 
						|
					<button
 | 
						|
						class="w3-button w3-bar-item w3-border-bottom"
 | 
						|
						@click=${this.copy_id}
 | 
						|
					>
 | 
						|
						Copy ID
 | 
						|
					</button>
 | 
						|
					${this.drafts[this.message?.id] === undefined
 | 
						|
						? html`
 | 
						|
								<button class="w3-button w3-bar-item" @click=${this.show_reply}>
 | 
						|
									↩️ Reply
 | 
						|
								</button>
 | 
						|
							`
 | 
						|
						: undefined}
 | 
						|
					<button
 | 
						|
						class="w3-button w3-bar-item w3-border-bottom"
 | 
						|
						@click=${this.react}
 | 
						|
					>
 | 
						|
						👍 React
 | 
						|
					</button>
 | 
						|
					${formats.map(
 | 
						|
						([format, name]) => html`
 | 
						|
							<button
 | 
						|
								class="w3-button w3-bar-item"
 | 
						|
								style=${format == this.format ? 'font-weight: bold' : ''}
 | 
						|
								@click=${() => (this.format = format)}
 | 
						|
							>
 | 
						|
								${name}
 | 
						|
							</button>
 | 
						|
						`
 | 
						|
					)}
 | 
						|
				</div>
 | 
						|
			</div>
 | 
						|
		`;
 | 
						|
	}
 | 
						|
 | 
						|
	render_header() {
 | 
						|
		let is_encrypted = this.message?.decrypted
 | 
						|
			? html`<span class="w3-bar-item">🔓</span>`
 | 
						|
			: typeof this.message?.content == 'string'
 | 
						|
				? html`<span class="w3-bar-item">🔒</span>`
 | 
						|
				: undefined;
 | 
						|
		return html`
 | 
						|
			<header class="w3-bar">
 | 
						|
				<span class="w3-bar-item">
 | 
						|
					${this.render_unread_icon()}<tf-user
 | 
						|
						id=${this.message.author}
 | 
						|
						.users=${this.users}
 | 
						|
					></tf-user>
 | 
						|
				</span>
 | 
						|
				${is_encrypted} ${this.render_menu()}
 | 
						|
				<div class="w3-bar-item w3-right" style="text-wrap: nowrap">
 | 
						|
					${new Date(this.message.timestamp).toLocaleString()}
 | 
						|
				</div>
 | 
						|
			</header>
 | 
						|
		`;
 | 
						|
	}
 | 
						|
 | 
						|
	render_frame(inner) {
 | 
						|
		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
 | 
						|
				class="w3-card-4 ${this.class_background()} w3-border-theme ${this
 | 
						|
					.depth == 0
 | 
						|
					? 'w3-margin-top'
 | 
						|
					: ''}"
 | 
						|
				style="overflow-wrap: anywhere; display: block; max-width: 100%"
 | 
						|
			>
 | 
						|
				${inner}
 | 
						|
			</div>
 | 
						|
		`;
 | 
						|
	}
 | 
						|
 | 
						|
	render_small_frame(inner) {
 | 
						|
		let self = this;
 | 
						|
		return this.render_frame(html`
 | 
						|
			${self.render_header()}
 | 
						|
			${self.format == 'raw'
 | 
						|
				? html`<div class="w3-container">${self.render_raw()}</div>`
 | 
						|
				: inner}
 | 
						|
			${self.render_votes()}
 | 
						|
			${(self.message.child_messages || []).map(
 | 
						|
				(x) => html`
 | 
						|
					<tf-message
 | 
						|
						.message=${x}
 | 
						|
						whoami=${self.whoami}
 | 
						|
						.users=${self.users}
 | 
						|
						.drafts=${self.drafts}
 | 
						|
						.expanded=${self.expanded}
 | 
						|
						channel=${self.channel}
 | 
						|
						channel_unread=${self.channel_unread}
 | 
						|
						.recent_reactions=${self.recent_reactions}
 | 
						|
						depth=${self.depth + 1}
 | 
						|
					></tf-message>
 | 
						|
				`
 | 
						|
			)}
 | 
						|
		`);
 | 
						|
	}
 | 
						|
 | 
						|
	render_actions() {
 | 
						|
		let content = this.get_content();
 | 
						|
		let reply =
 | 
						|
			this.drafts[this.message?.id] !== undefined
 | 
						|
				? html`
 | 
						|
						<div class="w3-section w3-container">
 | 
						|
							<tf-compose
 | 
						|
								whoami=${this.whoami}
 | 
						|
								.users=${this.users}
 | 
						|
								root=${content.root || this.message.id}
 | 
						|
								branch=${this.message.id}
 | 
						|
								.drafts=${this.drafts}
 | 
						|
								@tf-discard=${this.discard_reply}
 | 
						|
								author=${this.message.author}
 | 
						|
								.recent_reactions=${this.recent_reactions}
 | 
						|
							></tf-compose>
 | 
						|
						</div>
 | 
						|
					`
 | 
						|
				: undefined;
 | 
						|
		return html`
 | 
						|
			${reply}
 | 
						|
			<footer>${this.render_children()}</footer>
 | 
						|
		`;
 | 
						|
	}
 | 
						|
 | 
						|
	content_group_by_author() {
 | 
						|
		let sorted = this.message.messages
 | 
						|
			.map((x) => [
 | 
						|
				x.author,
 | 
						|
				x.content.following && x.content.blocking
 | 
						|
					? 'is following and blocking'
 | 
						|
					: x.content.following
 | 
						|
						? 'is following'
 | 
						|
						: x.content.blocking
 | 
						|
							? 'is blocking'
 | 
						|
							: x.content.blocking !== undefined
 | 
						|
								? 'is no longer blocking'
 | 
						|
								: x.content.following !== undefined
 | 
						|
									? 'is no longer following'
 | 
						|
									: '',
 | 
						|
				x.content.contact,
 | 
						|
				x,
 | 
						|
			])
 | 
						|
			.sort();
 | 
						|
		let result = [];
 | 
						|
		let last;
 | 
						|
		let group;
 | 
						|
		for (let row of sorted) {
 | 
						|
			if (last && last[0] == row[0] && last[1] == row[1]) {
 | 
						|
				group.push(row[2]);
 | 
						|
			} else {
 | 
						|
				if (group) {
 | 
						|
					result.push({author: last[0], action: last[1], users: group});
 | 
						|
				}
 | 
						|
				last = row;
 | 
						|
				group = [row[2]];
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if (group) {
 | 
						|
			result.push({author: last[0], action: last[1], users: group});
 | 
						|
		}
 | 
						|
		return result;
 | 
						|
	}
 | 
						|
 | 
						|
	channel_group_by_author() {
 | 
						|
		let sorted = this.message.messages
 | 
						|
			.map((x) => [
 | 
						|
				x.author,
 | 
						|
				x.content.subscribed ? 'subscribed to' : 'unsubscribed from',
 | 
						|
				x.content.channel,
 | 
						|
				x,
 | 
						|
			])
 | 
						|
			.sort();
 | 
						|
		let result = [];
 | 
						|
		let last;
 | 
						|
		let group;
 | 
						|
		for (let row of sorted) {
 | 
						|
			if (last && last[0] == row[0] && last[1] == row[1]) {
 | 
						|
				group.push(row[2]);
 | 
						|
			} else {
 | 
						|
				if (group) {
 | 
						|
					result.push({author: last[0], action: last[1], channels: group});
 | 
						|
				}
 | 
						|
				last = row;
 | 
						|
				group = [row[2]];
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if (group) {
 | 
						|
			result.push({author: last[0], action: last[1], channels: group});
 | 
						|
		}
 | 
						|
		return result;
 | 
						|
	}
 | 
						|
 | 
						|
	allow_unread() {
 | 
						|
		return (
 | 
						|
			this.channel == '@' ||
 | 
						|
			(!this.channel.startsWith('@') && !this.channel.startsWith('%'))
 | 
						|
		);
 | 
						|
	}
 | 
						|
 | 
						|
	render_unread_icon() {
 | 
						|
		return this.allow_unread() && this.message?.rowid >= this.channel_unread
 | 
						|
			? html`✉️`
 | 
						|
			: undefined;
 | 
						|
	}
 | 
						|
 | 
						|
	_render() {
 | 
						|
		let content = this.message?.content;
 | 
						|
		if (this.message?.decrypted?.type == 'post') {
 | 
						|
			content = this.message.decrypted;
 | 
						|
		}
 | 
						|
		let class_background = this.class_background();
 | 
						|
		let self = this;
 | 
						|
		if (this.message?.type === 'contact_group') {
 | 
						|
			if (this.expanded[this.expanded_key()]) {
 | 
						|
				return this.render_frame(html`
 | 
						|
					<div class="w3-padding">
 | 
						|
						${this.message.messages.map(
 | 
						|
							(x) =>
 | 
						|
								html`<tf-message
 | 
						|
									.message=${x}
 | 
						|
									whoami=${this.whoami}
 | 
						|
									.users=${this.users}
 | 
						|
									.drafts=${this.drafts}
 | 
						|
									.expanded=${this.expanded}
 | 
						|
									channel=${this.channel}
 | 
						|
									channel_unread=${this.channel_unread}
 | 
						|
									depth=${this.depth + 1}
 | 
						|
								></tf-message>`
 | 
						|
						)}
 | 
						|
					</div>
 | 
						|
					<button
 | 
						|
						class="w3-button w3-theme-d1 w3-block w3-bar"
 | 
						|
						style="box-sizing: border-box"
 | 
						|
						@click=${() => self.set_expanded(false)}
 | 
						|
					>
 | 
						|
						Collapse
 | 
						|
					</button>
 | 
						|
				`);
 | 
						|
			} else {
 | 
						|
				return this.render_frame(html`
 | 
						|
					<div class="w3-padding">
 | 
						|
						${this.content_group_by_author().map(
 | 
						|
							(x) => html`
 | 
						|
								<div>
 | 
						|
									<tf-user id=${x.author} .users=${this.users}></tf-user>
 | 
						|
									${x.action}
 | 
						|
									${x.users.map(
 | 
						|
										(y) => html`
 | 
						|
											<tf-user
 | 
						|
												id=${y}
 | 
						|
												.users=${this.users}
 | 
						|
												icon_only="true"
 | 
						|
											></tf-user>
 | 
						|
										`
 | 
						|
									)}
 | 
						|
								</div>
 | 
						|
							`
 | 
						|
						)}
 | 
						|
					</div>
 | 
						|
					<button
 | 
						|
						class="w3-button w3-theme-d1 w3-block w3-bar"
 | 
						|
						style="box-sizing: border-box"
 | 
						|
						@click=${() => self.set_expanded(true)}
 | 
						|
					>
 | 
						|
						Expand
 | 
						|
					</button>
 | 
						|
				`);
 | 
						|
			}
 | 
						|
		} else if (this.message?.type === 'channel_group') {
 | 
						|
			if (this.expanded[this.expanded_key()]) {
 | 
						|
				return this.render_frame(html`
 | 
						|
					<div class="w3-padding">
 | 
						|
						${this.message.messages.map(
 | 
						|
							(x) =>
 | 
						|
								html`<tf-message
 | 
						|
									.message=${x}
 | 
						|
									whoami=${this.whoami}
 | 
						|
									.users=${this.users}
 | 
						|
									.drafts=${this.drafts}
 | 
						|
									.expanded=${this.expanded}
 | 
						|
									channel=${this.channel}
 | 
						|
									channel_unread=${this.channel_unread}
 | 
						|
									depth=${this.depth + 1}
 | 
						|
								></tf-message>`
 | 
						|
						)}
 | 
						|
					</div>
 | 
						|
					<button
 | 
						|
						class="w3-button w3-theme-d1 w3-block w3-bar"
 | 
						|
						style="box-sizing: border-box"
 | 
						|
						@click=${() => self.set_expanded(false)}
 | 
						|
					>
 | 
						|
						Collapse
 | 
						|
					</button>
 | 
						|
				`);
 | 
						|
			} else {
 | 
						|
				return this.render_frame(html`
 | 
						|
					<div class="w3-padding">
 | 
						|
						${this.channel_group_by_author().map(
 | 
						|
							(x) => html`
 | 
						|
								<div>
 | 
						|
									<tf-user id=${x.author} .users=${this.users}></tf-user>
 | 
						|
									${x.action}
 | 
						|
									${x.channels.map(
 | 
						|
										(y) => html` <tf-tag tag=${'#' + y}></tf-tag> `
 | 
						|
									)}
 | 
						|
								</div>
 | 
						|
							`
 | 
						|
						)}
 | 
						|
					</div>
 | 
						|
					<button
 | 
						|
						class="w3-button w3-theme-d1 w3-block w3-bar"
 | 
						|
						style="box-sizing: border-box"
 | 
						|
						@click=${() => self.set_expanded(true)}
 | 
						|
					>
 | 
						|
						Expand
 | 
						|
					</button>
 | 
						|
				`);
 | 
						|
			}
 | 
						|
		} else if (this.message.placeholder) {
 | 
						|
			return this.render_frame(
 | 
						|
				html`<div>
 | 
						|
					<div class="w3-bar">
 | 
						|
						<a
 | 
						|
							class="w3-bar-item w3-panel w3-round-xlarge w3-theme-d1 w3-margin w3-button"
 | 
						|
							target="_top"
 | 
						|
							href=${'#' + encodeURIComponent(this.message?.id)}
 | 
						|
						>
 | 
						|
							This message is not currently available.
 | 
						|
						</a>
 | 
						|
						<div class="w3-bar-item w3-right">
 | 
						|
							<button class="w3-button w3-theme-d1" @click=${this.toggle_menu}>
 | 
						|
								%
 | 
						|
							</button>
 | 
						|
							<div
 | 
						|
								class="w3-dropdown-content w3-bar-block w3-card-4 w3-theme-l1"
 | 
						|
								style="right: 48px"
 | 
						|
							>
 | 
						|
								<a
 | 
						|
									target="_top"
 | 
						|
									class="w3-button w3-bar-item"
 | 
						|
									href=${'#' + encodeURIComponent(this.message?.id)}
 | 
						|
									>View Message</a
 | 
						|
								>
 | 
						|
								<button
 | 
						|
									class="w3-button w3-bar-item w3-border-bottom"
 | 
						|
									@click=${this.copy_id}
 | 
						|
								>
 | 
						|
									Copy ID
 | 
						|
								</button>
 | 
						|
							</div>
 | 
						|
						</div>
 | 
						|
					</div>
 | 
						|
					<div>${this.render_votes()}</div>
 | 
						|
					${(this.message.child_messages || []).map(
 | 
						|
						(x) => html`
 | 
						|
							<tf-message
 | 
						|
								.message=${x}
 | 
						|
								whoami=${this.whoami}
 | 
						|
								.users=${this.users}
 | 
						|
								.drafts=${this.drafts}
 | 
						|
								.expanded=${this.expanded}
 | 
						|
								channel=${this.channel}
 | 
						|
								channel_unread=${this.channel_unread}
 | 
						|
								depth=${this.depth + 1}
 | 
						|
							></tf-message>
 | 
						|
						`
 | 
						|
					)}
 | 
						|
				</div>`
 | 
						|
			);
 | 
						|
		} else if (typeof content?.type === 'string') {
 | 
						|
			if (content.type == 'about') {
 | 
						|
				let name;
 | 
						|
				let image;
 | 
						|
				let description;
 | 
						|
				if (content.name !== undefined) {
 | 
						|
					name = html`<div><b>Name:</b> ${content.name}</div>`;
 | 
						|
				}
 | 
						|
				if (content.image !== undefined) {
 | 
						|
					image = html`
 | 
						|
						<div @click=${this.body_click}><img src=${'/' + (typeof content.image?.link == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
 | 
						|
					`;
 | 
						|
				}
 | 
						|
				if (content.description !== undefined) {
 | 
						|
					description = html`
 | 
						|
						<div style="flex: 1 0 50%; overflow-wrap: anywhere">
 | 
						|
							<div>${unsafeHTML(tfutils.markdown(content.description))}</div>
 | 
						|
						</div>
 | 
						|
					`;
 | 
						|
				}
 | 
						|
				let update =
 | 
						|
					content.about == this.message.author
 | 
						|
						? html`<div style="font-weight: bold">Updated profile.</div>`
 | 
						|
						: html`<div style="font-weight: bold">
 | 
						|
								Updated profile for
 | 
						|
								<tf-user id=${content.about} .users=${this.users}></tf-user>.
 | 
						|
							</div>`;
 | 
						|
				return this.render_small_frame(html`
 | 
						|
					<div class="w3-container">
 | 
						|
						<p>${update} ${name} ${image} ${description}</p>
 | 
						|
					</div>
 | 
						|
				`);
 | 
						|
			} else if (content.type == 'contact') {
 | 
						|
				switch (this.format) {
 | 
						|
					case 'message':
 | 
						|
					default:
 | 
						|
						return this.render_frame(html`
 | 
						|
							<div class="w3-bar">
 | 
						|
								<div class="w3-bar-item">
 | 
						|
									<tf-user
 | 
						|
										id=${this.message.author}
 | 
						|
										.users=${this.users}
 | 
						|
									></tf-user>
 | 
						|
									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>
 | 
						|
								${this.render_menu()} ${this.render_votes()}
 | 
						|
								${this.render_actions()}
 | 
						|
							</div>
 | 
						|
						`);
 | 
						|
						break;
 | 
						|
					case 'raw':
 | 
						|
						return this.render_frame(html`
 | 
						|
							${this.render_header()}
 | 
						|
							<div class="w3-container">${this.render_raw()}</div>
 | 
						|
							${this.render_votes()} ${this.render_actions()}
 | 
						|
						</div>
 | 
						|
						`);
 | 
						|
						break;
 | 
						|
				}
 | 
						|
			} else if (content.type == 'post') {
 | 
						|
				let self = this;
 | 
						|
				let body;
 | 
						|
				switch (this.format) {
 | 
						|
					case 'raw':
 | 
						|
						body = this.render_raw();
 | 
						|
						break;
 | 
						|
					case 'md':
 | 
						|
						body = html`<code
 | 
						|
							style="white-space: pre-wrap; overflow-wrap: anywhere"
 | 
						|
							>${content.text}</code
 | 
						|
						>`;
 | 
						|
						break;
 | 
						|
					case 'message':
 | 
						|
						body = unsafeHTML(tfutils.markdown(content.text));
 | 
						|
						break;
 | 
						|
					case 'decrypted':
 | 
						|
						body = this.render_json(content);
 | 
						|
						break;
 | 
						|
				}
 | 
						|
				let content_warning = html`
 | 
						|
					<div
 | 
						|
						class="w3-panel w3-round-xlarge w3-theme-l4 w3"
 | 
						|
						style="cursor: pointer"
 | 
						|
						@click=${(x) => this.toggle_expanded(':cw')}
 | 
						|
					>
 | 
						|
						<p>${content.contentWarning}</p>
 | 
						|
						<p class="w3-small">
 | 
						|
							${this.is_expanded(':cw') ? 'Show less' : 'Show more'}
 | 
						|
						</p>
 | 
						|
					</div>
 | 
						|
				`;
 | 
						|
				let content_html = html`
 | 
						|
					${this.render_channels()}
 | 
						|
					<div @click=${this.body_click}>${body}</div>
 | 
						|
					${this.render_mentions()}
 | 
						|
				`;
 | 
						|
				let payload = content.contentWarning
 | 
						|
					? self.expanded[(this.message.id || '') + ':cw']
 | 
						|
						? html` ${content_warning} ${content_html} `
 | 
						|
						: content_warning
 | 
						|
					: content_html;
 | 
						|
				return this.render_frame(html`
 | 
						|
					${this.render_header()}
 | 
						|
					<div class="w3-container">${payload}</div>
 | 
						|
					${this.render_votes()} ${this.render_actions()}
 | 
						|
				</div>
 | 
						|
				`);
 | 
						|
			} else if (content.type === 'issue') {
 | 
						|
				return this.render_frame(html`
 | 
						|
					${this.render_header()} ${content.text} ${this.render_votes()}
 | 
						|
					<footer class="w3-container">
 | 
						|
						<button class="w3-button w3-theme-d1" @click=${this.react}>
 | 
						|
							React
 | 
						|
						</button>
 | 
						|
						${this.render_children()}
 | 
						|
					</footer>
 | 
						|
				`);
 | 
						|
			} else if (content.type === 'blog') {
 | 
						|
				let self = this;
 | 
						|
				tfrpc.rpc.get_blob(content.blog).then(function (data) {
 | 
						|
					self.blog_data = data;
 | 
						|
				});
 | 
						|
				let payload = this.expanded[(this.message.id || '') + ':blog']
 | 
						|
					? html`<div>
 | 
						|
							${this.blog_data
 | 
						|
								? unsafeHTML(tfutils.markdown(this.blog_data))
 | 
						|
								: 'Loading...'}
 | 
						|
						</div>`
 | 
						|
					: undefined;
 | 
						|
				let body;
 | 
						|
				switch (this.format) {
 | 
						|
					case 'raw':
 | 
						|
						body = this.render_raw();
 | 
						|
						break;
 | 
						|
					case 'md':
 | 
						|
						body = content.summary;
 | 
						|
						break;
 | 
						|
					case 'message':
 | 
						|
						body = html`
 | 
						|
							<div
 | 
						|
								style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
 | 
						|
								@click=${(x) => self.toggle_expanded(':blog')}>
 | 
						|
								<h2>${content.title}</h2>
 | 
						|
								<div style="display: flex; flex-direction: row">
 | 
						|
									<img src=/${content.thumbnail}/view></img>
 | 
						|
									<span>${content.summary}</span>
 | 
						|
								</div>
 | 
						|
							</div>
 | 
						|
							${payload}
 | 
						|
						`;
 | 
						|
						break;
 | 
						|
				}
 | 
						|
				return this.render_frame(html`
 | 
						|
					${this.render_header()}
 | 
						|
					<div>${body}</div>
 | 
						|
					${this.render_mentions()} ${this.render_votes()}
 | 
						|
					${this.render_actions()}
 | 
						|
				`);
 | 
						|
			} else if (content.type === 'pub') {
 | 
						|
				return this.render_small_frame(
 | 
						|
					html` <style>
 | 
						|
							span {
 | 
						|
								overflow-wrap: anywhere;
 | 
						|
							}
 | 
						|
						</style>
 | 
						|
						<div class="w3-padding">
 | 
						|
							<div>
 | 
						|
								🍻
 | 
						|
								<tf-user
 | 
						|
									.users=${this.users}
 | 
						|
									id=${content.address.key}
 | 
						|
								></tf-user>
 | 
						|
							</div>
 | 
						|
							<pre>${content.address.host}:${content.address.port}</pre>
 | 
						|
						</div>`
 | 
						|
				);
 | 
						|
			} else if (content.type === 'channel') {
 | 
						|
				return this.render_small_frame(html`
 | 
						|
					<div class="w3-container">
 | 
						|
						<p>
 | 
						|
							${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
 | 
						|
							<a href=${'#' + encodeURIComponent('#' + content.channel)}
 | 
						|
								>#${content.channel}</a
 | 
						|
							>
 | 
						|
						</p>
 | 
						|
					</div>
 | 
						|
				`);
 | 
						|
			} else if (typeof this.message.content == 'string') {
 | 
						|
				if (this.message?.decrypted) {
 | 
						|
					if (this.format == 'decrypted') {
 | 
						|
						return this.render_small_frame(
 | 
						|
							html`<span class="w3-container">🔓</span> ${this.render_json(
 | 
						|
									this.message.decrypted
 | 
						|
								)}`
 | 
						|
						);
 | 
						|
					} else {
 | 
						|
						return this.render_small_frame(
 | 
						|
							html`<span class="w3-container">🔓</span>
 | 
						|
								<div class="w3-container">${this.message.decrypted.type}</div>`
 | 
						|
						);
 | 
						|
					}
 | 
						|
				} else {
 | 
						|
					return this.render_small_frame();
 | 
						|
				}
 | 
						|
			} else {
 | 
						|
				return this.render_small_frame(
 | 
						|
					html`<div class="w3-container">
 | 
						|
						<p><b>type</b>: ${content.type}</p>
 | 
						|
					</div>`
 | 
						|
				);
 | 
						|
			}
 | 
						|
		} else if (typeof this.message.content == 'string') {
 | 
						|
			return this.render_small_frame();
 | 
						|
		} else {
 | 
						|
			return this.render_small_frame(this.render_raw());
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	render() {
 | 
						|
		return html`
 | 
						|
			<style>
 | 
						|
				${generate_theme()}
 | 
						|
			</style>
 | 
						|
			${this._render()}
 | 
						|
		`;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
customElements.define('tf-message', TfMessageElement);
 |