All checks were successful
		
		
	
	Build Tilde Friends / Build-All (push) Successful in 18m35s
				
			
		
			
				
	
	
		
			577 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			577 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import {LitElement, cache, html, unsafeHTML, until} from './lit-all.min.js';
 | |
| import * as tfrpc from '/static/tfrpc.js';
 | |
| import {styles, generate_theme} from './tf-styles.js';
 | |
| 
 | |
| class TfTabNewsFeedElement extends LitElement {
 | |
| 	static get properties() {
 | |
| 		return {
 | |
| 			whoami: {type: String},
 | |
| 			users: {type: Object},
 | |
| 			hash: {type: String},
 | |
| 			following: {type: Array},
 | |
| 			messages: {type: Array},
 | |
| 			drafts: {type: Object},
 | |
| 			expanded: {type: Object},
 | |
| 			channels_unread: {type: Object},
 | |
| 			channels_latest: {type: Object},
 | |
| 			loading: {type: Number},
 | |
| 			time_range: {type: Array},
 | |
| 			time_loading: {type: Array},
 | |
| 			private_messages: {type: Array},
 | |
| 			grouped_private_messages: {type: Object},
 | |
| 			recent_reactions: {type: Array},
 | |
| 		};
 | |
| 	}
 | |
| 
 | |
| 	static styles = styles;
 | |
| 
 | |
| 	constructor() {
 | |
| 		super();
 | |
| 		let self = this;
 | |
| 		this.whoami = null;
 | |
| 		this.users = {};
 | |
| 		this.hash = '#';
 | |
| 		this.following = [];
 | |
| 		this.drafts = {};
 | |
| 		this.expanded = {};
 | |
| 		this.channels_unread = {};
 | |
| 		this.channels_latest = {};
 | |
| 		this.start_time = new Date().valueOf();
 | |
| 		this.time_range = [0, 0];
 | |
| 		this.time_loading = undefined;
 | |
| 		this.recent_reactions = [];
 | |
| 		this.loading = 0;
 | |
| 	}
 | |
| 
 | |
| 	channel() {
 | |
| 		return this.hash.startsWith('##')
 | |
| 			? this.hash.substring(2)
 | |
| 			: this.hash.substring(1);
 | |
| 	}
 | |
| 
 | |
| 	async _fetch_related_messages(messages) {
 | |
| 		let refs = await tfrpc.rpc.query(
 | |
| 			`
 | |
| 				WITH
 | |
| 					news AS (
 | |
| 						SELECT value AS id FROM json_each(?)
 | |
| 					)
 | |
| 				SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id
 | |
| 				UNION
 | |
| 				SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id
 | |
| 			`,
 | |
| 			[JSON.stringify(messages.map((x) => x.id))]
 | |
| 		);
 | |
| 		let related_messages = await tfrpc.rpc.query(
 | |
| 			`
 | |
| 				SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | |
| 				FROM messages
 | |
| 				JOIN json_each(?2) refs ON messages.id = refs.value
 | |
| 				JOIN json_each(?1) AS following ON messages.author = following.value
 | |
| 			`,
 | |
| 			[JSON.stringify(this.following), JSON.stringify(refs.map((x) => x.ref))]
 | |
| 		);
 | |
| 		let combined = [].concat(messages, related_messages);
 | |
| 		let refs2 = await tfrpc.rpc.query(
 | |
| 			`
 | |
| 				WITH
 | |
| 					news AS (
 | |
| 						SELECT value AS id FROM json_each(?)
 | |
| 					)
 | |
| 				SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id
 | |
| 				UNION
 | |
| 				SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id
 | |
| 			`,
 | |
| 			[JSON.stringify(combined.map((x) => x.id))]
 | |
| 		);
 | |
| 		let t0 = new Date();
 | |
| 		let result = [].concat(
 | |
| 			combined,
 | |
| 			await tfrpc.rpc.query(
 | |
| 				`
 | |
| 				SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | |
| 				FROM json_each(?2) refs
 | |
| 				JOIN messages ON messages.id = refs.value
 | |
| 				JOIN json_each(?1) following ON messages.author = following.value
 | |
| 				WHERE messages.content ->> 'type' != 'post'
 | |
| 			`,
 | |
| 				[
 | |
| 					JSON.stringify(this.following),
 | |
| 					JSON.stringify(refs2.map((x) => x.ref)),
 | |
| 				]
 | |
| 			)
 | |
| 		);
 | |
| 		let t1 = new Date();
 | |
| 		console.log((t1 - t0) / 1000);
 | |
| 		return result;
 | |
| 	}
 | |
| 
 | |
| 	async fetch_messages(start_time, end_time) {
 | |
| 		this.dispatchEvent(
 | |
| 			new CustomEvent('loadmessages', {
 | |
| 				bubbles: true,
 | |
| 				composed: true,
 | |
| 			})
 | |
| 		);
 | |
| 		this.time_loading = [start_time, end_time];
 | |
| 		let result;
 | |
| 		const k_max_results = 64;
 | |
| 		if (this.hash == '#@') {
 | |
| 			result = await tfrpc.rpc.query(
 | |
| 				`
 | |
| 					WITH mentions AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | |
| 						FROM messages_fts(?1)
 | |
| 						JOIN messages ON messages.rowid = messages_fts.rowid
 | |
| 						JOIN json_each(?2) AS following ON messages.author = following.value
 | |
| 						WHERE
 | |
| 							messages.author != ?1 AND
 | |
| 							(?3 IS NULL OR messages.timestamp >= ?3) AND messages.timestamp < ?4
 | |
| 						ORDER BY timestamp DESC limit ?5)
 | |
| 					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | |
| 						FROM mentions
 | |
| 						JOIN messages_refs ON mentions.id = messages_refs.ref
 | |
| 						JOIN messages ON messages_refs.message = messages.id
 | |
| 					UNION
 | |
| 					SELECT TRUE AS is_primary, * FROM mentions
 | |
| 				`,
 | |
| 				[
 | |
| 					'"' + this.whoami.replace('"', '""') + '"',
 | |
| 					JSON.stringify(this.following),
 | |
| 					start_time,
 | |
| 					end_time,
 | |
| 					k_max_results,
 | |
| 				]
 | |
| 			);
 | |
| 		} else if (this.hash.startsWith('#@')) {
 | |
| 			result = await tfrpc.rpc.query(
 | |
| 				`
 | |
| 					WITH
 | |
| 						selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | |
| 							FROM messages
 | |
| 							WHERE messages.author = ?1 AND (?2 IS NULL OR messages.timestamp >= 2) AND messages.timestamp < ?3
 | |
| 							ORDER BY sequence DESC LIMIT ?4
 | |
| 						)
 | |
| 					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | |
| 						FROM selected
 | |
| 						JOIN messages_refs ON selected.id = messages_refs.ref
 | |
| 						JOIN messages ON messages_refs.message = messages.id
 | |
| 					UNION
 | |
| 					SELECT TRUE AS is_primary, * FROM selected
 | |
| 				`,
 | |
| 				[this.hash.substring(1), start_time, end_time, k_max_results]
 | |
| 			);
 | |
| 		} else if (this.hash.startsWith('#%')) {
 | |
| 			result = await tfrpc.rpc.query(
 | |
| 				`
 | |
| 					SELECT TRUE AS is_primary, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | |
| 					FROM messages
 | |
| 					WHERE messages.id = ?1
 | |
| 					UNION
 | |
| 					SELECT FALSE AS is_primary, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | |
| 					FROM messages JOIN messages_refs
 | |
| 					ON messages.id = messages_refs.message
 | |
| 					WHERE messages_refs.ref = ?1
 | |
| 				`,
 | |
| 				[this.hash.substring(1)]
 | |
| 			);
 | |
| 		} else if (this.hash.startsWith('##')) {
 | |
| 			let t0 = new Date();
 | |
| 			let initial_messages = await tfrpc.rpc.query(
 | |
| 				`
 | |
| 					WITH
 | |
| 						all_news AS (
 | |
| 							SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | |
| 								FROM messages
 | |
| 								JOIN json_each(?) AS following ON messages.author = following.value
 | |
| 								WHERE messages.content ->> 'channel' = ?4 AND messages.content ->> 'type' != 'vote'
 | |
| 							UNION
 | |
| 							SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | |
| 								FROM messages_refs
 | |
| 								JOIN messages ON messages.id = messages_refs.message
 | |
| 								JOIN json_each(?1) AS following ON messages.author = following.value
 | |
| 								WHERE messages_refs.ref = '#' || ?4 AND messages.content ->> 'type' != 'vote'
 | |
| 							)
 | |
| 					SELECT TRUE AS is_primary, all_news.* FROM all_news
 | |
| 						WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
 | |
| 						ORDER BY all_news.timestamp DESC LIMIT ?5
 | |
| 				`,
 | |
| 				[
 | |
| 					JSON.stringify(this.following),
 | |
| 					start_time,
 | |
| 					end_time,
 | |
| 					this.hash.substring(2),
 | |
| 					k_max_results,
 | |
| 				]
 | |
| 			);
 | |
| 			let t1 = new Date();
 | |
| 			result = await this._fetch_related_messages(initial_messages);
 | |
| 			let t2 = new Date();
 | |
| 			console.log(
 | |
| 				`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}`
 | |
| 			);
 | |
| 		} else if (this.hash.startsWith('#🔐')) {
 | |
| 			let ids =
 | |
| 				this.hash == '#🔐' ? [] : this.hash.substring('#🔐'.length).split(',');
 | |
| 			result = await tfrpc.rpc.query(
 | |
| 				`
 | |
| 					SELECT TRUE AS is_primary, messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | |
| 					FROM messages
 | |
| 					JOIN json_each(?1) AS private_messages ON messages.id = private_messages.value
 | |
| 					WHERE
 | |
| 						(?2 IS NULL OR (messages.timestamp >= ?2)) AND messages.timestamp < ?3 AND
 | |
| 						json(messages.content) LIKE '"%'
 | |
| 					ORDER BY messages.rowid DESC LIMIT ?4
 | |
| 				`,
 | |
| 				[
 | |
| 					JSON.stringify(
 | |
| 						this.grouped_private_messages?.[JSON.stringify(ids)]?.map(
 | |
| 							(x) => x.id
 | |
| 						) ?? []
 | |
| 					),
 | |
| 					start_time,
 | |
| 					end_time,
 | |
| 					k_max_results,
 | |
| 				]
 | |
| 			);
 | |
| 			result = (await this.decrypt(result)).filter((x) => x.decrypted);
 | |
| 		} else if (this.hash == '#👍') {
 | |
| 			result = await tfrpc.rpc.query(
 | |
| 				`
 | |
| 					WITH votes AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | |
| 						FROM messages
 | |
| 						JOIN json_each(?1) AS following ON messages.author = following.value
 | |
| 						WHERE
 | |
| 							messages.content ->> 'type' = 'vote' AND
 | |
| 							(?2 IS NULL OR messages.timestamp >= ?2) AND messages.timestamp < ?3
 | |
| 						ORDER BY timestamp DESC limit ?4)
 | |
| 					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | |
| 						FROM votes
 | |
| 						JOIN messages ON messages.id = votes.content ->> '$.vote.link'
 | |
| 					UNION
 | |
| 					SELECT TRUE AS is_primary, * FROM votes
 | |
| 				`,
 | |
| 				[JSON.stringify(this.following), start_time, end_time, k_max_results]
 | |
| 			);
 | |
| 		} else {
 | |
| 			let t0 = new Date();
 | |
| 			let initial_messages = await tfrpc.rpc.query(
 | |
| 				`
 | |
| 					WITH
 | |
| 						channels AS (SELECT '#' || value AS value FROM json_each(?5))
 | |
| 					SELECT TRUE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | |
| 							FROM messages
 | |
| 							JOIN json_each(?1) AS following ON messages.author = following.value
 | |
| 							WHERE messages.timestamp < ?3 AND (?2 IS NULL OR messages.timestamp >= ?2) AND
 | |
| 								messages.content ->> 'type' != 'vote' AND
 | |
| 								(messages.content ->> 'root' IS NULL OR (
 | |
| 									NOT EXISTS (SELECT * FROM messages root JOIN channels ON ('#' || (root.content ->> 'channel')) = channels.value WHERE root.id = messages.content ->> 'root') AND
 | |
| 									NOT EXISTS (SELECT * FROM messages root JOIN messages_refs ON root.id = messages.content ->> 'root' JOIN channels ON messages_refs.message = root.id AND messages_refs.ref = channels.value)
 | |
| 								)) AND
 | |
| 								(messages.content ->> 'channel' IS NULL OR ('#' || (messages.content ->> 'channel')) NOT IN (SELECT * FROM channels)) AND
 | |
| 								NOT EXISTS (SELECT * FROM messages_refs JOIN channels ON messages_refs.message = messages.id AND messages_refs.ref = channels.value)
 | |
| 					ORDER BY timestamp DESC LIMIT ?4
 | |
| 				`,
 | |
| 				[
 | |
| 					JSON.stringify(this.following),
 | |
| 					start_time,
 | |
| 					end_time,
 | |
| 					k_max_results,
 | |
| 					JSON.stringify(Object.keys(this.channels_latest)),
 | |
| 				]
 | |
| 			);
 | |
| 			let t1 = new Date();
 | |
| 			result = await this._fetch_related_messages(initial_messages);
 | |
| 			let t2 = new Date();
 | |
| 			console.log(
 | |
| 				`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}`
 | |
| 			);
 | |
| 		}
 | |
| 		this.time_loading = undefined;
 | |
| 		return result;
 | |
| 	}
 | |
| 
 | |
| 	update_time_range_from_messages(messages) {
 | |
| 		let only_primary = messages.filter((x) => x.is_primary);
 | |
| 		this.time_range = [
 | |
| 			only_primary.reduce(
 | |
| 				(accumulator, current) => Math.min(accumulator, current.timestamp),
 | |
| 				this.time_range[0]
 | |
| 			),
 | |
| 			only_primary.reduce(
 | |
| 				(accumulator, current) => Math.max(accumulator, current.timestamp),
 | |
| 				this.time_range[1]
 | |
| 			),
 | |
| 		];
 | |
| 	}
 | |
| 
 | |
| 	unread_allowed() {
 | |
| 		return (
 | |
| 			this.hash == '#@' ||
 | |
| 			(!this.hash.startsWith('#%') && !this.hash.startsWith('#@'))
 | |
| 		);
 | |
| 	}
 | |
| 
 | |
| 	async load_more() {
 | |
| 		this.loading++;
 | |
| 		this.loading_canceled = false;
 | |
| 		try {
 | |
| 			let more = [];
 | |
| 			let last_start_time = this.time_range[0];
 | |
| 			try {
 | |
| 				more = await this.fetch_messages(null, last_start_time);
 | |
| 			} catch (e) {
 | |
| 				console.log(e);
 | |
| 			}
 | |
| 			this.update_time_range_from_messages(
 | |
| 				more.filter((x) => x.timestamp < last_start_time)
 | |
| 			);
 | |
| 			this.messages = await this.decrypt([...more, ...this.messages]);
 | |
| 		} finally {
 | |
| 			this.loading--;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	cancel_load() {
 | |
| 		this.loading_canceled = true;
 | |
| 	}
 | |
| 
 | |
| 	async decrypt(messages) {
 | |
| 		let result = [];
 | |
| 		for (let message of messages) {
 | |
| 			let content;
 | |
| 			try {
 | |
| 				content = JSON.parse(message?.content);
 | |
| 			} catch {}
 | |
| 			if (typeof content === 'string') {
 | |
| 				let decrypted;
 | |
| 				try {
 | |
| 					decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
 | |
| 				} catch {}
 | |
| 				if (decrypted) {
 | |
| 					try {
 | |
| 						message.decrypted = JSON.parse(decrypted);
 | |
| 					} catch {
 | |
| 						message.decrypted = decrypted;
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 			result.push(message);
 | |
| 		}
 | |
| 		return result;
 | |
| 	}
 | |
| 
 | |
| 	merge_messages(old_messages, new_messages) {
 | |
| 		let old_by_id = Object.fromEntries(old_messages.map((x) => [x.id, x]));
 | |
| 		return new_messages.map((x) => (old_by_id[x.id] ? old_by_id[x.id] : x));
 | |
| 	}
 | |
| 
 | |
| 	async load_latest() {
 | |
| 		this.loading++;
 | |
| 		let now = new Date().valueOf();
 | |
| 		let end_time = now + 24 * 60 * 60 * 1000;
 | |
| 		let messages = [];
 | |
| 		try {
 | |
| 			messages = await this.fetch_messages(this.time_range[0], end_time);
 | |
| 			messages = await this.decrypt(messages);
 | |
| 			this.update_time_range_from_messages(
 | |
| 				messages.filter(
 | |
| 					(x) => x.timestamp >= this.time_range[0] && x.timestamp < end_time
 | |
| 				)
 | |
| 			);
 | |
| 		} finally {
 | |
| 			this.loading--;
 | |
| 		}
 | |
| 		this.messages = this.merge_messages(
 | |
| 			this.messages,
 | |
| 			Object.values(
 | |
| 				Object.fromEntries(
 | |
| 					[...this.messages, ...messages]
 | |
| 						.sort((x, y) => x.timestamp - y.timestamp)
 | |
| 						.slice(-1024)
 | |
| 						.map((x) => [x.id, x])
 | |
| 				)
 | |
| 			)
 | |
| 		);
 | |
| 		console.log('done loading latest messages.');
 | |
| 	}
 | |
| 
 | |
| 	async load_messages() {
 | |
| 		let start_time = new Date();
 | |
| 		let self = this;
 | |
| 		this.loading++;
 | |
| 		let messages = [];
 | |
| 		let original_hash = this.hash;
 | |
| 		try {
 | |
| 			if (this._messages_hash !== this.hash) {
 | |
| 				this.messages = [];
 | |
| 				this._messages_hash = this.hash;
 | |
| 			}
 | |
| 			this._messages_following = JSON.stringify(this.following);
 | |
| 			this._private_messages =
 | |
| 				JSON.stringify(this.private_messages) +
 | |
| 				JSON.stringify(this.grouped_private_messages);
 | |
| 			this._channels_latest = JSON.stringify(
 | |
| 				Object.keys(this.channels_latest ?? {})
 | |
| 			);
 | |
| 			let now = new Date().valueOf();
 | |
| 			let start_time = now - 24 * 60 * 60 * 1000;
 | |
| 			this.start_time = start_time;
 | |
| 			this.time_range = [now + 24 * 60 * 60 * 1000, now + 24 * 60 * 60 * 1000];
 | |
| 			messages = await this.fetch_messages(null, this.time_range[1]);
 | |
| 			this.update_time_range_from_messages(
 | |
| 				messages.filter((x) => x.timestamp < this.time_range[1])
 | |
| 			);
 | |
| 			messages = await this.decrypt(messages);
 | |
| 		} finally {
 | |
| 			this.loading--;
 | |
| 		}
 | |
| 		if (this.hash == original_hash) {
 | |
| 			this.messages = this.merge_messages(this.messages, messages);
 | |
| 		}
 | |
| 		this.time_loading = undefined;
 | |
| 		console.log(
 | |
| 			`loading ${messages.length} messages done for ${self.whoami} in ${(new Date() - start_time) / 1000}s`
 | |
| 		);
 | |
| 	}
 | |
| 
 | |
| 	mark_all_read() {
 | |
| 		let newest = this.messages.reduce(
 | |
| 			(accumulator, current) => Math.max(accumulator, current.rowid),
 | |
| 			this.channels_latest[this.channel()] ?? -1
 | |
| 		);
 | |
| 		if (newest >= 0) {
 | |
| 			this.dispatchEvent(
 | |
| 				new CustomEvent('channelsetunread', {
 | |
| 					bubbles: true,
 | |
| 					composed: true,
 | |
| 					detail: {
 | |
| 						channel: this.channel(),
 | |
| 						unread: newest + 1,
 | |
| 					},
 | |
| 				})
 | |
| 			);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	close_private_chat() {
 | |
| 		this.mark_all_read();
 | |
| 		this.dispatchEvent(
 | |
| 			new CustomEvent('closeprivatechat', {
 | |
| 				bubbles: true,
 | |
| 				composed: true,
 | |
| 				detail: {
 | |
| 					key: JSON.stringify(
 | |
| 						this.hash == '#🔐'
 | |
| 							? []
 | |
| 							: this.hash.substring('#🔐'.length).split(',')
 | |
| 					),
 | |
| 				},
 | |
| 			})
 | |
| 		);
 | |
| 		tfrpc.rpc.setHash('#');
 | |
| 	}
 | |
| 
 | |
| 	render_close_chat_button() {
 | |
| 		if (this.hash.startsWith('#🔐')) {
 | |
| 			return html`
 | |
| 				<button class="w3-button w3-theme-d1" @click=${this.close_private_chat}>
 | |
| 					Close Chat
 | |
| 				</button>
 | |
| 			`;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	render() {
 | |
| 		if (
 | |
| 			!this.messages ||
 | |
| 			this._messages_hash !== this.hash ||
 | |
| 			this._messages_following !== JSON.stringify(this.following) ||
 | |
| 			this._private_messages !==
 | |
| 				JSON.stringify(this.private_messages) +
 | |
| 					JSON.stringify(this.grouped_private_messages) ||
 | |
| 			this._channels_latest !==
 | |
| 				JSON.stringify(Object.keys(this.channels_latest))
 | |
| 		) {
 | |
| 			console.log(
 | |
| 				`loading messages for ${this.whoami} (following ${this.following.length})`
 | |
| 			);
 | |
| 			this.load_messages();
 | |
| 		}
 | |
| 		let more;
 | |
| 		if (!this.hash.startsWith('#%')) {
 | |
| 			more = html`
 | |
| 				<p>
 | |
| 					${this.unread_allowed()
 | |
| 						? html`
 | |
| 								<button
 | |
| 									class="w3-button w3-theme-d1"
 | |
| 									@click=${this.mark_all_read}
 | |
| 								>
 | |
| 									Mark All Read
 | |
| 								</button>
 | |
| 							`
 | |
| 						: undefined}
 | |
| 					<button
 | |
| 						?disabled=${this.loading}
 | |
| 						class="w3-button w3-theme-d1"
 | |
| 						@click=${this.load_more}
 | |
| 					>
 | |
| 						Load More
 | |
| 					</button>
 | |
| 					<button
 | |
| 						class=${'w3-button w3-theme-d1' + (this.loading ? '' : ' w3-hide')}
 | |
| 						@click=${this.cancel_load}
 | |
| 					>
 | |
| 						Cancel
 | |
| 					</button>
 | |
| 					<span
 | |
| 						>Showing
 | |
| 						${new Date(
 | |
| 							this.time_loading
 | |
| 								? Math.min(this.time_loading[0], this.time_range[0])
 | |
| 								: this.time_range[0]
 | |
| 						).toLocaleDateString()}
 | |
| 						-
 | |
| 						${new Date(
 | |
| 							this.time_loading
 | |
| 								? Math.max(this.time_loading[1], this.time_range[1])
 | |
| 								: this.time_range[1]
 | |
| 						).toLocaleDateString()}.</span
 | |
| 					>
 | |
| 				</p>
 | |
| 			`;
 | |
| 		}
 | |
| 		return cache(html`
 | |
| 			<style>
 | |
| 				${generate_theme()}
 | |
| 			</style>
 | |
| 			${this.unread_allowed()
 | |
| 				? html`<button
 | |
| 						class="w3-button w3-theme-d1"
 | |
| 						@click=${this.mark_all_read}
 | |
| 					>
 | |
| 						Mark All Read
 | |
| 					</button>`
 | |
| 				: undefined}
 | |
| 			${this.render_close_chat_button()}
 | |
| 			<tf-news
 | |
| 				id="news"
 | |
| 				whoami=${this.whoami}
 | |
| 				.users=${this.users}
 | |
| 				.messages=${this.messages}
 | |
| 				.following=${this.following}
 | |
| 				.drafts=${this.drafts}
 | |
| 				.expanded=${this.expanded}
 | |
| 				hash=${this.hash}
 | |
| 				channel=${this.channel()}
 | |
| 				channel_unread=${this.channels_unread?.[this.channel()]}
 | |
| 				.recent_reactions=${this.recent_reactions}
 | |
| 				@mark_all_read=${this.mark_all_read}
 | |
| 			></tf-news>
 | |
| 			${more}
 | |
| 		`);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
 |