All checks were successful
		
		
	
	Build Tilde Friends / Build-All (push) Successful in 18m1s
				
			
		
			
				
	
	
		
			288 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			288 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import {LitElement, html, unsafeHTML, repeat, until} from './lit-all.min.js';
 | |
| import * as tfrpc from '/static/tfrpc.js';
 | |
| import {styles, generate_theme} from './tf-styles.js';
 | |
| 
 | |
| class TfNewsElement extends LitElement {
 | |
| 	static get properties() {
 | |
| 		return {
 | |
| 			whoami: {type: String},
 | |
| 			users: {type: Object},
 | |
| 			messages: {type: Array},
 | |
| 			following: {type: Array},
 | |
| 			drafts: {type: Object},
 | |
| 			expanded: {type: Object},
 | |
| 			channel: {type: String},
 | |
| 			channel_unread: {type: Number},
 | |
| 			recent_reactions: {type: Array},
 | |
| 			hash: {type: String},
 | |
| 		};
 | |
| 	}
 | |
| 
 | |
| 	static styles = styles;
 | |
| 
 | |
| 	constructor() {
 | |
| 		super();
 | |
| 		let self = this;
 | |
| 		this.whoami = null;
 | |
| 		this.users = {};
 | |
| 		this.messages = [];
 | |
| 		this.following = [];
 | |
| 		this.drafts = {};
 | |
| 		this.expanded = {};
 | |
| 		this.channel_unread = -1;
 | |
| 		this.recent_reactions = [];
 | |
| 	}
 | |
| 
 | |
| 	process_messages(messages) {
 | |
| 		let self = this;
 | |
| 		let messages_by_id = {};
 | |
| 
 | |
| 		console.log('processing', messages.length, 'messages');
 | |
| 
 | |
| 		function ensure_message(id, rowid) {
 | |
| 			let found = messages_by_id[id];
 | |
| 			if (found) {
 | |
| 				return found;
 | |
| 			} else {
 | |
| 				let added = {
 | |
| 					rowid: rowid,
 | |
| 					id: id,
 | |
| 					placeholder: true,
 | |
| 					content: '"placeholder"',
 | |
| 					parent_message: undefined,
 | |
| 					child_messages: [],
 | |
| 					votes: [],
 | |
| 				};
 | |
| 				messages_by_id[id] = added;
 | |
| 				return added;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		function link_message(message) {
 | |
| 			if (message.content.type === 'vote') {
 | |
| 				let parent = ensure_message(message.content.vote.link, message.rowid);
 | |
| 				if (!parent.votes) {
 | |
| 					parent.votes = [];
 | |
| 				}
 | |
| 				parent.votes.push(message);
 | |
| 				message.parent_message = message.content.vote.link;
 | |
| 			} else if (message.content.type == 'post') {
 | |
| 				if (message.content.root) {
 | |
| 					if (typeof message.content.root === 'string') {
 | |
| 						let m = ensure_message(message.content.root, message.rowid);
 | |
| 						if (!m.child_messages) {
 | |
| 							m.child_messages = [];
 | |
| 						}
 | |
| 						m.child_messages.push(message);
 | |
| 						message.parent_message = message.content.root;
 | |
| 					} else {
 | |
| 						let m = ensure_message(message.content.root[0], message.rowid);
 | |
| 						if (!m.child_messages) {
 | |
| 							m.child_messages = [];
 | |
| 						}
 | |
| 						m.child_messages.push(message);
 | |
| 						message.parent_message = message.content.root[0];
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		for (let message of messages) {
 | |
| 			message.votes = [];
 | |
| 			message.parent_message = undefined;
 | |
| 			message.child_messages = undefined;
 | |
| 		}
 | |
| 
 | |
| 		for (let message of messages) {
 | |
| 			try {
 | |
| 				message.content = JSON.parse(message.content);
 | |
| 			} catch {}
 | |
| 			if (!messages_by_id[message.id]) {
 | |
| 				messages_by_id[message.id] = message;
 | |
| 				link_message(message);
 | |
| 			} else if (messages_by_id[message.id].placeholder) {
 | |
| 				let placeholder = messages_by_id[message.id];
 | |
| 				messages_by_id[message.id] = message;
 | |
| 				message.parent_message = placeholder.parent_message;
 | |
| 				message.child_messages = placeholder.child_messages;
 | |
| 				message.votes = placeholder.votes;
 | |
| 				if (
 | |
| 					placeholder.parent_message &&
 | |
| 					messages_by_id[placeholder.parent_message]
 | |
| 				) {
 | |
| 					let children =
 | |
| 						messages_by_id[placeholder.parent_message].child_messages;
 | |
| 					children.splice(children.indexOf(placeholder), 1);
 | |
| 					children.push(message);
 | |
| 				}
 | |
| 				link_message(message);
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return messages_by_id;
 | |
| 	}
 | |
| 
 | |
| 	update_latest_subtree_timestamp(messages) {
 | |
| 		let latest = 0;
 | |
| 		for (let message of messages || []) {
 | |
| 			if (message.latest_subtree_timestamp === undefined) {
 | |
| 				message.latest_subtree_timestamp = Math.max(
 | |
| 					message.timestamp ?? 0,
 | |
| 					this.update_latest_subtree_timestamp(message.child_messages)
 | |
| 				);
 | |
| 			}
 | |
| 			latest = Math.max(latest, message.latest_subtree_timestamp);
 | |
| 		}
 | |
| 		return latest;
 | |
| 	}
 | |
| 
 | |
| 	finalize_messages(messages_by_id) {
 | |
| 		function recursive_sort(messages, top) {
 | |
| 			if (messages) {
 | |
| 				if (top) {
 | |
| 					messages.sort(
 | |
| 						(a, b) => b.latest_subtree_timestamp - a.latest_subtree_timestamp
 | |
| 					);
 | |
| 				} else {
 | |
| 					messages.sort((a, b) => a.timestamp - b.timestamp);
 | |
| 				}
 | |
| 				for (let message of messages) {
 | |
| 					recursive_sort(message.child_messages, false);
 | |
| 				}
 | |
| 				return messages.map((x) => Object.assign({}, x));
 | |
| 			} else {
 | |
| 				return {};
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		let roots = Object.values(messages_by_id).filter((x) => !x.parent_message);
 | |
| 		this.update_latest_subtree_timestamp(roots);
 | |
| 		return recursive_sort(roots, true);
 | |
| 	}
 | |
| 
 | |
| 	group_messages(messages) {
 | |
| 		let result = [];
 | |
| 		let group = [];
 | |
| 		let type = undefined;
 | |
| 		for (let message of messages) {
 | |
| 			if (
 | |
| 				message?.content?.type === 'contact' ||
 | |
| 				message?.content?.type === 'channel'
 | |
| 			) {
 | |
| 				if (type && message.content.type !== type) {
 | |
| 					if (group.length == 1) {
 | |
| 						result.push(group[0]);
 | |
| 						group = [];
 | |
| 					} else if (group.length > 1) {
 | |
| 						result.push({
 | |
| 							rowid: Math.max(...group.map((x) => x.rowid)),
 | |
| 							type: `${type}_group`,
 | |
| 							messages: group,
 | |
| 						});
 | |
| 						group = [];
 | |
| 					}
 | |
| 				}
 | |
| 				type = message.content.type;
 | |
| 				group.push(message);
 | |
| 			} else {
 | |
| 				if (group.length == 1) {
 | |
| 					result.push(group[0]);
 | |
| 					group = [];
 | |
| 				} else if (group.length > 1) {
 | |
| 					result.push({
 | |
| 						rowid: Math.max(...group.map((x) => x.rowid)),
 | |
| 						type: `${type}_group`,
 | |
| 						messages: group,
 | |
| 					});
 | |
| 					group = [];
 | |
| 				}
 | |
| 				result.push(message);
 | |
| 				type = undefined;
 | |
| 			}
 | |
| 		}
 | |
| 		if (group.length == 1) {
 | |
| 			result.push(group[0]);
 | |
| 			group = [];
 | |
| 		} else if (group.length > 1) {
 | |
| 			result.push({
 | |
| 				rowid: Math.max(...group.map((x) => x.rowid)),
 | |
| 				type: `${type}_group`,
 | |
| 				messages: group,
 | |
| 			});
 | |
| 		}
 | |
| 		return result;
 | |
| 	}
 | |
| 
 | |
| 	unread_allowed() {
 | |
| 		return !this.hash?.startsWith('#%') && !this.hash?.startsWith('#@');
 | |
| 	}
 | |
| 
 | |
| 	load_and_render(messages) {
 | |
| 		let messages_by_id = this.process_messages(messages);
 | |
| 		let final_messages = this.group_messages(
 | |
| 			this.finalize_messages(messages_by_id)
 | |
| 		);
 | |
| 		let unread_rowid = -1;
 | |
| 		if (this.unread_allowed()) {
 | |
| 			for (let message of final_messages) {
 | |
| 				if (message.rowid >= this.channel_unread) {
 | |
| 					unread_rowid = message.rowid;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		return html`
 | |
| 			<style>
 | |
| 				${generate_theme()}
 | |
| 			</style>
 | |
| 			<div>
 | |
| 				${repeat(
 | |
| 					final_messages,
 | |
| 					(x) => x.id,
 | |
| 					(x) => html`
 | |
| 						<tf-message
 | |
| 							.message=${x}
 | |
| 							whoami=${this.whoami}
 | |
| 							.users=${this.users}
 | |
| 							.drafts=${this.drafts}
 | |
| 							.expanded=${this.expanded}
 | |
| 							collapsed="true"
 | |
| 							channel=${this.channel}
 | |
| 							channel_unread=${this.channel_unread}
 | |
| 							.recent_reactions=${this.recent_reactions}
 | |
| 						></tf-message>
 | |
| 						${x.rowid == unread_rowid
 | |
| 							? html`<div style="display: flex; flex-direction: row">
 | |
| 									<div
 | |
| 										style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
 | |
| 									></div>
 | |
| 									<button
 | |
| 										style="color: #f00; padding: 8px"
 | |
| 										class="w3-button"
 | |
| 										@click=${() =>
 | |
| 											this.dispatchEvent(
 | |
| 												new Event('mark_all_read', {
 | |
| 													bubbles: true,
 | |
| 													composed: true,
 | |
| 												})
 | |
| 											)}
 | |
| 									>
 | |
| 										unread
 | |
| 									</button>
 | |
| 									<div
 | |
| 										style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
 | |
| 									></div>
 | |
| 								</div>`
 | |
| 							: undefined}
 | |
| 					`
 | |
| 				)}
 | |
| 			</div>
 | |
| 		`;
 | |
| 	}
 | |
| 
 | |
| 	render() {
 | |
| 		return this.load_and_render(this.messages || []);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| customElements.define('tf-news', TfNewsElement);
 |