forked from cory/tildefriends
		
	
		
			
				
	
	
		
			471 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			471 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import {
 | ||
| 	LitElement,
 | ||
| 	cache,
 | ||
| 	keyed,
 | ||
| 	html,
 | ||
| 	unsafeHTML,
 | ||
| 	until,
 | ||
| } from './lit-all.min.js';
 | ||
| import * as tfrpc from '/static/tfrpc.js';
 | ||
| import {styles, generate_theme} from './tf-styles.js';
 | ||
| 
 | ||
| class TfTabNewsElement extends LitElement {
 | ||
| 	static get properties() {
 | ||
| 		return {
 | ||
| 			whoami: {type: String},
 | ||
| 			users: {type: Object},
 | ||
| 			hash: {type: String},
 | ||
| 			following: {type: Array},
 | ||
| 			drafts: {type: Object},
 | ||
| 			expanded: {type: Object},
 | ||
| 			loading: {type: Boolean},
 | ||
| 			channels: {type: Array},
 | ||
| 			channels_unread: {type: Object},
 | ||
| 			channels_latest: {type: Object},
 | ||
| 			connections: {type: Array},
 | ||
| 			private_messages: {type: Array},
 | ||
| 			grouped_private_messages: {type: Object},
 | ||
| 			visible_private_messages: {type: Object},
 | ||
| 			recent_reactions: {type: Array},
 | ||
| 			peer_exchange: {type: Boolean},
 | ||
| 			is_administrator: {type: Boolean},
 | ||
| 			stay_connected: {type: Boolean},
 | ||
| 		};
 | ||
| 	}
 | ||
| 
 | ||
| 	static styles = styles;
 | ||
| 
 | ||
| 	constructor() {
 | ||
| 		super();
 | ||
| 		let self = this;
 | ||
| 		this.whoami = null;
 | ||
| 		this.users = {};
 | ||
| 		this.hash = '#';
 | ||
| 		this.following = [];
 | ||
| 		this.cache = {};
 | ||
| 		this.drafts = {};
 | ||
| 		this.expanded = {};
 | ||
| 		this.channels_unread = {};
 | ||
| 		this.channels_latest = {};
 | ||
| 		this.channels = [];
 | ||
| 		this.connections = [];
 | ||
| 		this.recent_reactions = [];
 | ||
| 		tfrpc.rpc.localStorageGet('drafts').then(function (d) {
 | ||
| 			self.drafts = JSON.parse(d || '{}');
 | ||
| 		});
 | ||
| 		this.check_peer_exchange();
 | ||
| 	}
 | ||
| 
 | ||
| 	connectedCallback() {
 | ||
| 		super.connectedCallback();
 | ||
| 		document.body.addEventListener('keypress', this.on_keypress.bind(this));
 | ||
| 	}
 | ||
| 
 | ||
| 	disconnectedCallback() {
 | ||
| 		super.disconnectedCallback();
 | ||
| 		document.body.removeEventListener('keypress', this.on_keypress.bind(this));
 | ||
| 	}
 | ||
| 
 | ||
| 	async check_peer_exchange() {
 | ||
| 		if (await tfrpc.rpc.isAdministrator()) {
 | ||
| 			this.peer_exchange = await tfrpc.rpc.globalSettingsGet('peer_exchange');
 | ||
| 		} else {
 | ||
| 			this.peer_exchange = undefined;
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	load_latest() {
 | ||
| 		let news = this.shadowRoot?.getElementById('news');
 | ||
| 		if (news) {
 | ||
| 			news.load_latest();
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	draft(event) {
 | ||
| 		let id = event.detail.id || '';
 | ||
| 		let previous = this.drafts[id];
 | ||
| 		if (event.detail.draft !== undefined) {
 | ||
| 			this.drafts[id] = event.detail.draft;
 | ||
| 		} else {
 | ||
| 			delete this.drafts[id];
 | ||
| 		}
 | ||
| 		this.drafts = Object.assign({}, this.drafts);
 | ||
| 		tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
 | ||
| 	}
 | ||
| 
 | ||
| 	on_expand(event) {
 | ||
| 		if (event.detail.expanded) {
 | ||
| 			let expand = {};
 | ||
| 			expand[event.detail.id] = true;
 | ||
| 			this.expanded = Object.assign({}, this.expanded, expand);
 | ||
| 		} else {
 | ||
| 			delete this.expanded[event.detail.id];
 | ||
| 			this.expanded = Object.assign({}, this.expanded);
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	on_keypress(event) {
 | ||
| 		if (event.target === document.body && event.key == '.') {
 | ||
| 			this.show_more();
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	unread_status(channel) {
 | ||
| 		if (channel === undefined) {
 | ||
| 			if (
 | ||
| 				Object.keys(this.channels_unread).some((x) => this.unread_status(x))
 | ||
| 			) {
 | ||
| 				return '✉️ ';
 | ||
| 			}
 | ||
| 		} else if (channel?.startsWith('🔐')) {
 | ||
| 			let key = JSON.stringify(channel.substring('🔐'.length).split(','));
 | ||
| 			if (this.grouped_private_messages?.[key]) {
 | ||
| 				let grouped_latest = Math.max(
 | ||
| 					...this.grouped_private_messages?.[key]?.map((x) => x.rowid)
 | ||
| 				);
 | ||
| 				if (
 | ||
| 					this.channels_unread[channel] === undefined ||
 | ||
| 					grouped_latest > this.channels_unread[channel]
 | ||
| 				) {
 | ||
| 					return '✉️ ';
 | ||
| 				}
 | ||
| 			}
 | ||
| 		} else if (
 | ||
| 			this.channels_latest[channel] &&
 | ||
| 			this.channels_latest[channel] > 0 &&
 | ||
| 			(this.channels_unread[channel] === undefined ||
 | ||
| 				this.channels_unread[channel] <= this.channels_latest[channel])
 | ||
| 		) {
 | ||
| 			return '✉️ ';
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	show_sidebar() {
 | ||
| 		this.renderRoot.getElementById('sidebar').style.display = 'block';
 | ||
| 		this.renderRoot.getElementById('sidebar_overlay').style.display = 'block';
 | ||
| 	}
 | ||
| 
 | ||
| 	hide_sidebar() {
 | ||
| 		this.renderRoot.getElementById('sidebar').style.display = 'none';
 | ||
| 		this.renderRoot.getElementById('sidebar_overlay').style.display = 'none';
 | ||
| 	}
 | ||
| 
 | ||
| 	async channel_toggle_subscribed() {
 | ||
| 		let channel = this.hash.substring(2);
 | ||
| 		let subscribed = this.channels.indexOf(channel) != -1;
 | ||
| 		subscribed = !subscribed;
 | ||
| 
 | ||
| 		await tfrpc.rpc.appendMessage(this.whoami, {
 | ||
| 			type: 'channel',
 | ||
| 			channel: channel,
 | ||
| 			subscribed: subscribed,
 | ||
| 		});
 | ||
| 		if (subscribed) {
 | ||
| 			this.channels = [].concat([channel], this.channels).sort();
 | ||
| 		} else {
 | ||
| 			this.channels = this.channels.filter((x) => x != channel);
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	channel() {
 | ||
| 		return this.hash.startsWith('##') ? this.hash.substring(2) : undefined;
 | ||
| 	}
 | ||
| 
 | ||
| 	compare_follows(a, b) {
 | ||
| 		return b[1].ts > a[1].ts ? 1 : b[1].ts < a[1].ts ? -1 : 0;
 | ||
| 	}
 | ||
| 
 | ||
| 	suggested_follows() {
 | ||
| 		/*
 | ||
| 		 ** Filter out people who have used future timestamps so that they aren't
 | ||
| 		 ** pinned at the top.
 | ||
| 		 */
 | ||
| 		let self = this;
 | ||
| 		let now = new Date().valueOf();
 | ||
| 		return Object.entries(this.users)
 | ||
| 			.filter((x) => x[1].ts < now)
 | ||
| 			.filter((x) => x[1].follow_depth > 1)
 | ||
| 			.sort(self.compare_follows)
 | ||
| 			.slice(0, 8)
 | ||
| 			.map((x) => x[0]);
 | ||
| 	}
 | ||
| 
 | ||
| 	async enable_peer_exchange() {
 | ||
| 		await tfrpc.rpc.globalSettingsSet('peer_exchange', true);
 | ||
| 		await this.check_peer_exchange();
 | ||
| 	}
 | ||
| 
 | ||
| 	is_loading() {
 | ||
| 		return this.shadowRoot?.getElementById('news')?.loading;
 | ||
| 	}
 | ||
| 
 | ||
| 	render_sidebar() {
 | ||
| 		return html`
 | ||
| 			<div
 | ||
| 				class="w3-sidebar w3-bar-block w3-theme-d1 w3-collapse w3-animate-left"
 | ||
| 				style="width: 2in; left: 0; z-index: 5; box-sizing: border-box; top: 0"
 | ||
| 				id="sidebar"
 | ||
| 			>
 | ||
| 				<div
 | ||
| 					class="w3-right w3-button w3-hide-large"
 | ||
| 					@click=${this.hide_sidebar}
 | ||
| 				>
 | ||
| 					×
 | ||
| 				</div>
 | ||
| 				${this.hash.startsWith('##') &&
 | ||
| 				this.channels.indexOf(this.hash.substring(2)) == -1
 | ||
| 					? html`
 | ||
| 							<div class="w3-bar-item w3-theme-d2">Viewing</div>
 | ||
| 							<a
 | ||
| 								href="#"
 | ||
| 								class="w3-bar-item w3-button"
 | ||
| 								style="font-weight: bold"
 | ||
| 								>${this.hash.substring(1)}</a
 | ||
| 							>
 | ||
| 						`
 | ||
| 					: undefined}
 | ||
| 				<h4 class="w3-bar-item w3-theme-d2">Channels</h4>
 | ||
| 				<a
 | ||
| 					href="#"
 | ||
| 					class="w3-bar-item w3-button"
 | ||
| 					style=${this.hash == '#' ? 'font-weight: bold' : undefined}
 | ||
| 					>${this.unread_status('')}general</a
 | ||
| 				>
 | ||
| 				<a
 | ||
| 					href="#@"
 | ||
| 					class="w3-bar-item w3-button"
 | ||
| 					style=${this.hash == '#@' ? 'font-weight: bold' : undefined}
 | ||
| 					>${this.unread_status('@')}@mentions</a
 | ||
| 				>
 | ||
| 				<a
 | ||
| 					href="#👍"
 | ||
| 					class="w3-bar-item w3-button"
 | ||
| 					style=${this.hash == '#👍' ? 'font-weight: bold' : undefined}
 | ||
| 					>${this.unread_status('👍')}👍votes</a
 | ||
| 				>
 | ||
| 				${Object.keys(this?.visible_private_messages ?? [])
 | ||
| 					?.sort()
 | ||
| 					?.map(
 | ||
| 						(key) => html`
 | ||
| 							<a
 | ||
| 								href=${'#🔐' + JSON.parse(key).join(',')}
 | ||
| 								class="w3-bar-item w3-button"
 | ||
| 								style=${this.hash == '#🔐' + JSON.parse(key).join(',')
 | ||
| 									? 'font-weight: bold'
 | ||
| 									: undefined}
 | ||
| 								>${this.unread_status('🔐' + JSON.parse(key).join(','))}
 | ||
| 								${(key != '[]' ? JSON.parse(key) : [this.whoami]).map(
 | ||
| 									(id) => html`
 | ||
| 										<tf-user
 | ||
| 											id=${id}
 | ||
| 											nolink="true"
 | ||
| 											.users=${this.users}
 | ||
| 										></tf-user>
 | ||
| 									`
 | ||
| 								)}</a
 | ||
| 							>
 | ||
| 						`
 | ||
| 					)}
 | ||
| 				${Object.keys(this.drafts)
 | ||
| 					.sort()
 | ||
| 					.map(
 | ||
| 						(x) => html`
 | ||
| 							<a
 | ||
| 								href=${'#' + encodeURIComponent(x)}
 | ||
| 								class="w3-bar-item w3-button"
 | ||
| 								style="text-wrap: nowrap; text-overflow: ellipsis"
 | ||
| 								>📝 ${this.drafts[x]?.text ?? x}</a
 | ||
| 							>
 | ||
| 						`
 | ||
| 					)}
 | ||
| 				${this.channels.map(
 | ||
| 					(x) => html`
 | ||
| 						<a
 | ||
| 							href=${'#' + encodeURIComponent('#' + x)}
 | ||
| 							class="w3-bar-item w3-button"
 | ||
| 							style=${this.hash == '##' + x ? 'font-weight: bold' : undefined}
 | ||
| 							>${this.unread_status(x)}#${x}</a
 | ||
| 						>
 | ||
| 					`
 | ||
| 				)}
 | ||
| 
 | ||
| 				<a class="w3-bar-item w3-theme-d2 w3-button" href="#connections">
 | ||
| 					<h4 style="margin: 0">Connections</h4>
 | ||
| 				</a>
 | ||
| 				${this.connections?.filter((x) => x.id)?.length == 0
 | ||
| 					? html`
 | ||
| 							<button
 | ||
| 								class=${'w3-bar-item w3-button' +
 | ||
| 								(this.connections?.some((x) => x.flags.one_shot)
 | ||
| 									? ' w3-spin'
 | ||
| 									: '')}
 | ||
| 								@click=${() =>
 | ||
| 									this.dispatchEvent(
 | ||
| 										new Event('refresh', {bubbles: true, composed: true})
 | ||
| 									)}
 | ||
| 							>
 | ||
| 								↻ Sync now
 | ||
| 							</button>
 | ||
| 							<button
 | ||
| 								class="w3-bar-item w3-button w3-ripple"
 | ||
| 								@click=${() =>
 | ||
| 									this.dispatchEvent(
 | ||
| 										new Event('toggle_stay_connected', {
 | ||
| 											bubbles: true,
 | ||
| 											composed: true,
 | ||
| 										})
 | ||
| 									)}
 | ||
| 							>
 | ||
| 								<span style="display: inline-block; width: 1.8em"
 | ||
| 									>${this.stay_connected ? '🔗' : '⛓️💥'}</span
 | ||
| 								>
 | ||
| 								${this.stay_connected ? 'Online mode' : 'Passive mode'}
 | ||
| 							</button>
 | ||
| 							<button
 | ||
| 								class=${'w3-bar-item w3-button w3-border w3-leftbar w3-rightbar' +
 | ||
| 								(this.peer_exchange !== false ? ' w3-hide' : '')}
 | ||
| 								@click=${this.enable_peer_exchange}
 | ||
| 							>
 | ||
| 								🔍🌐 Use publicly advertised peers
 | ||
| 							</button>
 | ||
| 						`
 | ||
| 					: undefined}
 | ||
| 				${this.connections
 | ||
| 					.filter((x) => x.id)
 | ||
| 					.map(
 | ||
| 						(x) => html`
 | ||
| 							<tf-user
 | ||
| 								class="w3-bar-item"
 | ||
| 								style=${x.destroy_reason
 | ||
| 									? 'border-left: 4px solid red; border-right: 4px solid red'
 | ||
| 									: x.connected
 | ||
| 										? x.flags?.one_shot
 | ||
| 											? 'border-left: 4px solid blue; border-right: 4px solid blue'
 | ||
| 											: 'border-left: 4px solid green; border-right: 4px solid green'
 | ||
| 										: ''}
 | ||
| 								id=${x.id}
 | ||
| 								fallback_name=${x.host}
 | ||
| 								.users=${this.users}
 | ||
| 							></tf-user>
 | ||
| 						`
 | ||
| 					)}
 | ||
| 				<h4 class="w3-bar-item w3-theme-d2">Suggested Follows</h4>
 | ||
| 				${this.suggested_follows().map(
 | ||
| 					(x) => html`
 | ||
| 						<tf-user
 | ||
| 							class="w3-bar-item"
 | ||
| 							style="max-width: 100%"
 | ||
| 							id=${x}
 | ||
| 							.users=${this.users}
 | ||
| 						></tf-user>
 | ||
| 					`
 | ||
| 				)}
 | ||
| 			</div>
 | ||
| 			<div
 | ||
| 				class="w3-overlay"
 | ||
| 				id="sidebar_overlay"
 | ||
| 				@click=${this.hide_sidebar}
 | ||
| 			></div>
 | ||
| 		`;
 | ||
| 	}
 | ||
| 
 | ||
| 	render() {
 | ||
| 		let profile =
 | ||
| 			this.hash.startsWith('#@') && this.hash != '#@'
 | ||
| 				? keyed(
 | ||
| 						this.hash.substring(1),
 | ||
| 						html`<tf-profile
 | ||
| 							class="tf-profile"
 | ||
| 							id=${this.hash.substring(1)}
 | ||
| 							whoami=${this.whoami}
 | ||
| 							.users=${this.users}
 | ||
| 						></tf-profile>`
 | ||
| 					)
 | ||
| 				: undefined;
 | ||
| 		let edit_profile;
 | ||
| 		if (
 | ||
| 			!this.loading &&
 | ||
| 			this.users[this.whoami]?.name === undefined &&
 | ||
| 			this.hash.substring(1) != this.whoami
 | ||
| 		) {
 | ||
| 			edit_profile = html` <div
 | ||
| 				class="w3-panel w3-padding w3-round w3-card-4 w3-theme-l3"
 | ||
| 			>
 | ||
| 				ℹ️ Follow your identity link ☝️ above to edit your profile and set your
 | ||
| 				name.
 | ||
| 			</div>`;
 | ||
| 		}
 | ||
| 		return cache(html`
 | ||
| 			<style>
 | ||
| 				${generate_theme()}
 | ||
| 			</style>
 | ||
| 			${this.render_sidebar()}
 | ||
| 			<div
 | ||
| 				style="margin-left: 2in; padding: 0px; top: 0; height: 100vh; max-height: 100%; overflow: auto; contain: layout"
 | ||
| 				id="main"
 | ||
| 				class="w3-main"
 | ||
| 			>
 | ||
| 				<div style="padding: 8px">
 | ||
| 					<p>
 | ||
| 						${this.hash.startsWith('##')
 | ||
| 							? html`
 | ||
| 									<button
 | ||
| 										class="w3-button w3-theme-d1"
 | ||
| 										@click=${this.channel_toggle_subscribed}
 | ||
| 									>
 | ||
| 										${this.channels.indexOf(this.hash.substring(2)) != -1
 | ||
| 											? 'Unsubscribe from #'
 | ||
| 											: 'Subscribe to #'}${this.hash.substring(2)}
 | ||
| 									</button>
 | ||
| 								`
 | ||
| 							: undefined}
 | ||
| 					</p>
 | ||
| 					<div>
 | ||
| 						<div
 | ||
| 							id="show_sidebar"
 | ||
| 							class="w3-button w3-hide-large"
 | ||
| 							@click=${this.show_sidebar}
 | ||
| 						>
 | ||
| 							${this.unread_status()}☰
 | ||
| 						</div>
 | ||
| 						Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
 | ||
| 						${edit_profile}
 | ||
| 					</div>
 | ||
| 					<div>
 | ||
| 						<tf-compose
 | ||
| 							id="tf-compose"
 | ||
| 							whoami=${this.whoami}
 | ||
| 							.users=${this.users}
 | ||
| 							.drafts=${this.drafts}
 | ||
| 							@tf-draft=${this.draft}
 | ||
| 							.channel=${this.channel()}
 | ||
| 							.recipients=${this.hash.startsWith('#🔐')
 | ||
| 								? this.hash.substring('#🔐'.length).split(',')
 | ||
| 								: undefined}
 | ||
| 						></tf-compose>
 | ||
| 					</div>
 | ||
| 					${profile}
 | ||
| 					<tf-tab-news-feed
 | ||
| 						id="news"
 | ||
| 						whoami=${this.whoami}
 | ||
| 						.users=${this.users}
 | ||
| 						.following=${this.following}
 | ||
| 						hash=${this.hash}
 | ||
| 						.drafts=${this.drafts}
 | ||
| 						.expanded=${this.expanded}
 | ||
| 						@tf-draft=${this.draft}
 | ||
| 						@tf-expand=${this.on_expand}
 | ||
| 						.channels_unread=${this.channels_unread}
 | ||
| 						.channels_latest=${this.channels_latest}
 | ||
| 						.private_messages=${this.private_messages}
 | ||
| 						.grouped_private_messages=${this.grouped_private_messages}
 | ||
| 						.recent_reactions=${this.recent_reactions}
 | ||
| 					></tf-tab-news-feed>
 | ||
| 				</div>
 | ||
| 			</div>
 | ||
| 		`);
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| customElements.define('tf-tab-news', TfTabNewsElement);
 |