| 
									
										
										
										
											2025-09-10 19:38:38 -07:00
										 |  |  | <script> | 
					
						
							|  |  |  | 	document.addEventListener("topics_updated", ({ detail: { topics } }) => { | 
					
						
							|  |  |  | 		const topic_list = document.getElementById("topic-list"); | 
					
						
							|  |  |  | 		topic_list.innerHTML = ""; | 
					
						
							| 
									
										
										
										
											2025-10-10 16:39:44 -07:00
										 |  |  | 		for (const topic of topics.sort((lhs, rhs) => lhs.name.localeCompare(rhs.name))) { | 
					
						
							| 
									
										
										
										
											2025-09-10 19:38:38 -07:00
										 |  |  | 			topic_list.insertAdjacentHTML( | 
					
						
							|  |  |  | 				"beforeend", | 
					
						
							| 
									
										
										
										
											2025-09-12 15:26:13 -07:00
										 |  |  | 				`<li id="topic-selector-${topic.id}" class="topic" data-topic-selector-for="${topic.id}"><a href="#/topic/${topic.id}/chat">${topic.name}</a></li>`, | 
					
						
							| 
									
										
										
										
											2025-09-10 19:38:38 -07:00
										 |  |  | 			); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	}); | 
					
						
							| 
									
										
										
										
											2025-09-12 15:26:13 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-08 19:55:28 -07:00
										 |  |  | 	function update_topic_indicators(event) { | 
					
						
							|  |  |  | 		document | 
					
						
							|  |  |  | 			.querySelectorAll("[data-topic-selector-for]") | 
					
						
							|  |  |  | 			.forEach((element) => element.classList.remove("active")); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		const new_topic_id = event?.detail?.topic_id ?? document.body.dataset.topic; | 
					
						
							| 
									
										
										
										
											2025-09-12 15:26:13 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-08 19:55:28 -07:00
										 |  |  | 		if (!new_topic_id) { | 
					
						
							| 
									
										
										
										
											2025-09-12 15:26:13 -07:00
										 |  |  | 			return; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-08 19:55:28 -07:00
										 |  |  | 		document | 
					
						
							|  |  |  | 			.querySelectorAll(`[data-topic-selector-for="${new_topic_id}"]`) | 
					
						
							|  |  |  | 			.forEach((element) => element.classList.add("active")); | 
					
						
							| 
									
										
										
										
											2025-09-12 15:26:13 -07:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	document.addEventListener("topics_updated", update_topic_indicators); | 
					
						
							|  |  |  | 	document.addEventListener("topic_changed", update_topic_indicators); | 
					
						
							|  |  |  | 	document.addEventListener("user_logged_in", update_topic_indicators); | 
					
						
							| 
									
										
										
										
											2025-10-08 17:38:23 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-08 19:07:21 -07:00
										 |  |  | 	function clear_invite_popup() { | 
					
						
							|  |  |  | 		document.body.querySelectorAll(".invitepopover").forEach((element) => element.remove()); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-08 17:38:23 -07:00
										 |  |  | 	function generate_invite(click_event) { | 
					
						
							| 
									
										
										
										
											2025-10-08 19:07:21 -07:00
										 |  |  | 		click_event.preventDefault(); | 
					
						
							| 
									
										
										
										
											2025-10-08 17:38:23 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-08 19:07:21 -07:00
										 |  |  | 		const button = click_event.target; | 
					
						
							| 
									
										
										
										
											2025-10-08 17:38:23 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-08 19:07:21 -07:00
										 |  |  | 		clear_invite_popup(); | 
					
						
							| 
									
										
										
										
											2025-10-08 17:38:23 -07:00
										 |  |  | 		const invite_div = document.createElement("div"); | 
					
						
							|  |  |  | 		invite_div.classList.add("invitepopover"); | 
					
						
							| 
									
										
										
										
											2025-10-08 19:07:21 -07:00
										 |  |  | 		invite_div.innerHTML = ` | 
					
						
							|  |  |  | 		<div class="icon close" onclick="clear_invite_popup()"></div> | 
					
						
							|  |  |  | 		<form> | 
					
						
							| 
									
										
										
										
											2025-10-08 17:38:23 -07:00
										 |  |  | 			<input name="code" type="text" placeholder="Custom code (optional)"> | 
					
						
							|  |  |  | 			<button onclick="create_invite(event);">Generate</button> | 
					
						
							|  |  |  | 		</form>`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		document.body.appendChild(invite_div); | 
					
						
							|  |  |  | 		invite_div.style.left = button.getBoundingClientRect().left + "px"; | 
					
						
							|  |  |  | 		invite_div.style.top = button.getBoundingClientRect().top + "px"; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	async function create_invite(click_event) { | 
					
						
							|  |  |  | 		click_event.preventDefault(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		const button = click_event.target; | 
					
						
							|  |  |  | 		const invite_popover = document.body.querySelector(".invitepopover"); | 
					
						
							|  |  |  | 		if (!invite_popover) { | 
					
						
							|  |  |  | 			alert("Unknown error, try again."); | 
					
						
							|  |  |  | 			return; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		const user = document.body.dataset.user && JSON.parse(document.body.dataset.user); | 
					
						
							|  |  |  | 		if (!user) { | 
					
						
							|  |  |  | 			alert("You must be logged in."); | 
					
						
							|  |  |  | 			return; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		const form = button.closest("form"); | 
					
						
							|  |  |  | 		const code_input = form.querySelector('[name="code"]'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		const invite_code_response = await api.fetch(`/api/users/${user.id}/invites`, { | 
					
						
							|  |  |  | 			method: "POST", | 
					
						
							|  |  |  | 			json: { | 
					
						
							|  |  |  | 				code: code_input.value, | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if (!invite_code_response.ok) { | 
					
						
							|  |  |  | 			const error = await invite_code_response.json(); | 
					
						
							|  |  |  | 			return alert(error?.error?.message ?? error?.errors?.[0]?.message ?? "Unknown error."); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		const invite_code = await invite_code_response.json(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		invite_popover.innerHTML = ` | 
					
						
							|  |  |  | 			<div> | 
					
						
							| 
									
										
										
										
											2025-10-08 19:07:21 -07:00
										 |  |  | 				<div class="icon close" onclick="clear_invite_popup()"></div> | 
					
						
							| 
									
										
										
										
											2025-10-08 17:38:23 -07:00
										 |  |  | 				<div class="share-option"> | 
					
						
							|  |  |  | 					<span class="name">Code</span> | 
					
						
							|  |  |  | 					<input readonly type="text" name="code" value="${invite_code.code}" /> | 
					
						
							|  |  |  | 					<button onclick="navigator.clipboard.writeText('${invite_code.code}');" />Copy</button> | 
					
						
							|  |  |  | 				</div> | 
					
						
							|  |  |  | 				<div class="share-option"> | 
					
						
							|  |  |  | 					<span class="name">Link</span> | 
					
						
							|  |  |  | 					<input readonly type="text" name="code" value="${window.location.protocol + "//" + window.location.host + "/?invite_code=" + encodeURIComponent(invite_code.code)}" /> | 
					
						
							|  |  |  | 					<button onclick="navigator.clipboard.writeText('${window.location.protocol + "//" + window.location.host + "/?invite_code=" + encodeURIComponent(invite_code.code)}');" />Copy</button> | 
					
						
							|  |  |  | 				</div> | 
					
						
							|  |  |  | 				<button onclick="( () => document.querySelectorAll( '.invitepopover' ).forEach( (element) => element.remove() ) )()">Done</button> | 
					
						
							|  |  |  | 			</div>`; | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-09-10 19:38:38 -07:00
										 |  |  | </script> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <style type="text/css"> | 
					
						
							|  |  |  | 	main { | 
					
						
							|  |  |  | 		position: relative; | 
					
						
							|  |  |  | 		width: 100%; | 
					
						
							|  |  |  | 		height: 100%; | 
					
						
							|  |  |  | 		display: grid; | 
					
						
							|  |  |  | 		grid-template-columns: auto 1fr; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	@media screen and (max-width: 1200px) { | 
					
						
							|  |  |  | 		main { | 
					
						
							|  |  |  | 			grid-template-columns: auto; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	#sidebar { | 
					
						
							|  |  |  | 		z-index: 100; | 
					
						
							|  |  |  | 		background: var(--bg); | 
					
						
							|  |  |  | 		position: relative; | 
					
						
							|  |  |  | 		width: auto; | 
					
						
							|  |  |  | 		left: 0; | 
					
						
							|  |  |  | 		max-width: 32rem; | 
					
						
							|  |  |  | 		padding: 0.5rem; | 
					
						
							|  |  |  | 		transition: all ease-in-out 0.33s; | 
					
						
							|  |  |  | 		border-right: 1px solid var(--border-subtle); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	#sidebar #sidebar-toggle, | 
					
						
							|  |  |  | 	#sidebar #sidebar-toggle-icon { | 
					
						
							|  |  |  | 		opacity: 0; | 
					
						
							|  |  |  | 		display: none; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	@media screen and (max-width: 1200px) { | 
					
						
							|  |  |  | 		#sidebar { | 
					
						
							|  |  |  | 			position: absolute; | 
					
						
							|  |  |  | 			top: 0; | 
					
						
							|  |  |  | 			bottom: 0; | 
					
						
							|  |  |  | 			transform: translateX(-100%); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		#sidebar #sidebar-toggle-icon { | 
					
						
							|  |  |  | 			opacity: 1; | 
					
						
							|  |  |  | 			display: block; | 
					
						
							|  |  |  | 			position: absolute; | 
					
						
							| 
									
										
										
										
											2025-10-08 19:21:47 -07:00
										 |  |  | 			top: 0.1rem; | 
					
						
							|  |  |  | 			right: -2rem; | 
					
						
							| 
									
										
										
										
											2025-09-10 19:38:38 -07:00
										 |  |  | 			cursor: pointer; | 
					
						
							|  |  |  | 			transition: all ease-in-out 0.33s; | 
					
						
							|  |  |  | 			background: rgba(128, 128, 128, 0.5); | 
					
						
							|  |  |  | 			border-radius: 0 1rem 1rem 0; | 
					
						
							| 
									
										
										
										
											2025-10-08 19:21:47 -07:00
										 |  |  | 			padding: 0.25rem; | 
					
						
							| 
									
										
										
										
											2025-09-10 19:38:38 -07:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		#sidebar .icon { | 
					
						
							|  |  |  | 			transition: all ease-in-out 0.15s; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		#sidebar:has(#sidebar-toggle:checked) { | 
					
						
							|  |  |  | 			transform: translateX(0); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		#sidebar:has(#sidebar-toggle:checked) #sidebar-toggle-icon { | 
					
						
							|  |  |  | 			right: 0; | 
					
						
							|  |  |  | 			rotate: 180deg; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	#sidebar .title { | 
					
						
							|  |  |  | 		text-transform: uppercase; | 
					
						
							|  |  |  | 		font-size: small; | 
					
						
							|  |  |  | 		font-weight: bold; | 
					
						
							|  |  |  | 		line-height: 2rem; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	#sidebar #topic-creation-container { | 
					
						
							|  |  |  | 		margin-top: 0.5rem; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	#sidebar #topic-creation-container #toggle-topic-creation-form-button { | 
					
						
							|  |  |  | 		transform: scale(0.8); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	#sidebar .topic-list { | 
					
						
							|  |  |  | 		list-style-type: none; | 
					
						
							| 
									
										
										
										
											2025-09-16 19:54:43 -07:00
										 |  |  | 		margin-left: 1rem; | 
					
						
							| 
									
										
										
										
											2025-09-10 19:38:38 -07:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	#sidebar .topic-list > li.topic a:before { | 
					
						
							|  |  |  | 		position: absolute; | 
					
						
							|  |  |  | 		left: -1.75rem; | 
					
						
							|  |  |  | 		top: 0; | 
					
						
							|  |  |  | 		font-weight: bold; | 
					
						
							|  |  |  | 		font-size: x-large; | 
					
						
							|  |  |  | 		content: "#"; | 
					
						
							|  |  |  | 		color: var(--text); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	#sidebar .topic-list > li.topic a { | 
					
						
							|  |  |  | 		position: relative; | 
					
						
							|  |  |  | 		display: block; | 
					
						
							|  |  |  | 		width: 100%; | 
					
						
							|  |  |  | 		min-height: 1.5rem; | 
					
						
							|  |  |  | 		line-height: 1.5rem; | 
					
						
							|  |  |  | 		font-weight: bold; | 
					
						
							|  |  |  | 		font-size: large; | 
					
						
							|  |  |  | 		margin-left: 1.75rem; | 
					
						
							|  |  |  | 		text-decoration: none; | 
					
						
							| 
									
										
										
										
											2025-09-16 19:54:43 -07:00
										 |  |  | 		margin-bottom: 0.75rem; | 
					
						
							| 
									
										
										
										
											2025-09-10 19:38:38 -07:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	#sidebar .topic-list > li.topic.active a { | 
					
						
							|  |  |  | 		color: var(--accent); | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-10-08 17:38:23 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	.invitepopover { | 
					
						
							|  |  |  | 		position: fixed; | 
					
						
							|  |  |  | 		z-index: 1000; | 
					
						
							|  |  |  | 		background: var(--bg); | 
					
						
							|  |  |  | 		margin: 1rem; | 
					
						
							|  |  |  | 		border: 1px solid var(--border-normal); | 
					
						
							|  |  |  | 		border-radius: var(--border-radius); | 
					
						
							|  |  |  | 		padding: 1rem; | 
					
						
							| 
									
										
										
										
											2025-10-08 19:21:47 -07:00
										 |  |  | 		max-width: 90%; | 
					
						
							| 
									
										
										
										
											2025-10-08 17:38:23 -07:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	.invitepopover .share-option .name { | 
					
						
							|  |  |  | 		text-transform: uppercase; | 
					
						
							|  |  |  | 		min-width: 4rem; | 
					
						
							|  |  |  | 		display: inline-block; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-08 19:07:21 -07:00
										 |  |  | 	.invitepopover .icon.close { | 
					
						
							|  |  |  | 		cursor: pointer; | 
					
						
							|  |  |  | 		margin-right: -0.25rem; | 
					
						
							|  |  |  | 		margin-top: -0.25rem; | 
					
						
							|  |  |  | 		margin-bottom: 0.75rem; | 
					
						
							|  |  |  | 		margin-left: auto; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	.invitepopover .share-option input { | 
					
						
							| 
									
										
										
										
											2025-10-08 17:38:23 -07:00
										 |  |  | 		padding: 0.75rem; | 
					
						
							|  |  |  | 		margin: 0 1rem 1rem 0; | 
					
						
							|  |  |  | 		background: none; | 
					
						
							|  |  |  | 		color: var(--text); | 
					
						
							|  |  |  | 		border: 1px solid var(--border-highlight); | 
					
						
							|  |  |  | 		border-radius: var(--border-radius); | 
					
						
							|  |  |  | 		box-shadow: none; | 
					
						
							|  |  |  | 		font-size: large; | 
					
						
							| 
									
										
										
										
											2025-10-08 19:21:47 -07:00
										 |  |  | 		width: 100%; | 
					
						
							|  |  |  | 		max-width: 16rem; | 
					
						
							| 
									
										
										
										
											2025-10-08 17:38:23 -07:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-08 19:07:21 -07:00
										 |  |  | 	.invitepopover .share-option button { | 
					
						
							| 
									
										
										
										
											2025-10-08 17:38:23 -07:00
										 |  |  | 		padding: 1rem; | 
					
						
							|  |  |  | 		text-transform: uppercase; | 
					
						
							|  |  |  | 		border: 1px solid var(--border-subtle); | 
					
						
							|  |  |  | 		border-radius: var(--border-radius); | 
					
						
							|  |  |  | 		margin-left: -0.75rem; | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-10-08 19:21:47 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	@media screen and (max-width: 1200px) { | 
					
						
							|  |  |  | 		.invitepopover { | 
					
						
							|  |  |  | 			margin: 0; | 
					
						
							|  |  |  | 			padding: 0.5rem; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		.invitepopover .share-option .name { | 
					
						
							|  |  |  | 			display: block; | 
					
						
							|  |  |  | 			text-align: center; | 
					
						
							|  |  |  | 			margin-bottom: 1rem; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		.invitepopover .share-option button { | 
					
						
							|  |  |  | 			display: block; | 
					
						
							|  |  |  | 			margin: 0 auto 1rem; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2025-09-10 19:38:38 -07:00
										 |  |  | </style> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <div id="sidebar"> | 
					
						
							|  |  |  | 	<input type="checkbox" id="sidebar-toggle" /> | 
					
						
							|  |  |  | 	<label id="sidebar-toggle-icon" for="sidebar-toggle"> | 
					
						
							|  |  |  | 		<div class="icon right"></div> | 
					
						
							|  |  |  | 	</label> | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	<script> | 
					
						
							|  |  |  | 		const DEFAULT_AVATAR_URL = `//${window.location.host}/images/default_avatar.gif`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		new MutationObserver((mutations, observer) => { | 
					
						
							|  |  |  | 			mutations.forEach((mutation) => { | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 				const user = document.body.dataset.user | 
					
						
							|  |  |  | 					? JSON.parse(document.body.dataset.user) | 
					
						
							|  |  |  | 					: null; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				const user_bound_elements = document.querySelectorAll("[data-bind-to-user-field]"); | 
					
						
							|  |  |  | 				for (const user_bound_element of user_bound_elements) { | 
					
						
							|  |  |  | 					const key = | 
					
						
							|  |  |  | 						user_bound_element.dataset | 
					
						
							|  |  |  | 							.bindToUserField; /* I hate that it converts the name */ | 
					
						
							|  |  |  | 					const key_elements = key.split("."); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					let value = undefined; | 
					
						
							|  |  |  | 					if (user) { | 
					
						
							|  |  |  | 						let current = user; | 
					
						
							|  |  |  | 						for (const key_element of key_elements) { | 
					
						
							|  |  |  | 							current = current[key_element]; | 
					
						
							|  |  |  | 							if (!current) { | 
					
						
							|  |  |  | 								break; | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 						} | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 						value = current; | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					const target = | 
					
						
							|  |  |  | 						typeof user_bound_element.dataset.userFieldTarget === "string" && | 
					
						
							|  |  |  | 						user_bound_element.dataset.userFieldTarget.length > 0 | 
					
						
							|  |  |  | 							? user_bound_element.dataset.userFieldTarget | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 							: "innerHTML"; | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 					const default_value = | 
					
						
							|  |  |  | 						typeof user_bound_element.dataset.userFieldDefault === "string" && | 
					
						
							|  |  |  | 						user_bound_element.dataset.userFieldDefault.length > 0 | 
					
						
							|  |  |  | 							? user_bound_element.dataset.userFieldDefault | 
					
						
							| 
									
										
										
										
											2025-09-15 12:13:29 -07:00
										 |  |  | 							: ""; | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 					user_bound_element[target] = value ?? default_value; | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-16 20:11:42 -07:00
										 |  |  | 				const primary_color_setting = user?.meta?.primary_color; | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 				if (primary_color_setting) { | 
					
						
							|  |  |  | 					const root = document.querySelector(":root"); | 
					
						
							|  |  |  | 					root.style.setProperty("--base-color", primary_color_setting); | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 				} | 
					
						
							|  |  |  | 			}); | 
					
						
							|  |  |  | 		}).observe(document.body, { | 
					
						
							|  |  |  | 			attributes: true, | 
					
						
							|  |  |  | 			attributeFilter: ["data-user"], | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 	</script> | 
					
						
							|  |  |  | 	<style type="text/css"> | 
					
						
							|  |  |  | 		.profile-container { | 
					
						
							|  |  |  | 			max-width: 1024px; | 
					
						
							|  |  |  | 			padding: 1rem; | 
					
						
							| 
									
										
										
										
											2025-09-16 19:48:01 -07:00
										 |  |  | 			padding-bottom: 0; | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		.profile-container .avatar-container { | 
					
						
							|  |  |  | 			position: relative; | 
					
						
							|  |  |  | 			width: 100%; | 
					
						
							|  |  |  | 			max-width: 200px; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		.profile-container .avatar-container #user-avatar { | 
					
						
							|  |  |  | 			width: 100%; | 
					
						
							|  |  |  | 			height: 100%; | 
					
						
							|  |  |  | 			max-width: 100%; | 
					
						
							|  |  |  | 			max-height: 100%; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		.profile-container .avatar-container input[type="file"] { | 
					
						
							|  |  |  | 			position: absolute; | 
					
						
							|  |  |  | 			top: 0; | 
					
						
							|  |  |  | 			right: 0; | 
					
						
							|  |  |  | 			bottom: 0; | 
					
						
							|  |  |  | 			left: 0; | 
					
						
							|  |  |  | 			opacity: 0; | 
					
						
							|  |  |  | 			cursor: pointer; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	</style> | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 	<form class="profile-container"> | 
					
						
							|  |  |  | 		<script> | 
					
						
							|  |  |  | 			const profile_form = document.currentScript.closest("form"); | 
					
						
							| 
									
										
										
										
											2025-09-10 19:38:38 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 			document.addEventListener("DOMContentLoaded", () => { | 
					
						
							|  |  |  | 				const inputs = profile_form.querySelectorAll("input"); | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 				async function update_from_input(input) { | 
					
						
							|  |  |  | 					delete input.__debounce_timeout; | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 					if (!document.body.dataset.user) { | 
					
						
							|  |  |  | 						return; | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 					const user = JSON.parse(document.body.dataset.user); | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 					const updated_user = { ...user }; | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 					switch (input.name) { | 
					
						
							|  |  |  | 						case "meta.avatar": | 
					
						
							|  |  |  | 							const avatar = input.files[0]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							if (!avatar || !avatar.type || !avatar.type.includes("image")) { | 
					
						
							|  |  |  | 								return alert( | 
					
						
							|  |  |  | 									"You must select a valid image to upload as your avatar.", | 
					
						
							|  |  |  | 								); | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							// TODO: actually enforce this on the upload in serverus somehow | 
					
						
							|  |  |  | 							if (avatar.size > 512_000) { | 
					
						
							|  |  |  | 								return alert("512K is the largest allowed avatar size."); | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							const avatar_upload_body = new FormData(); | 
					
						
							|  |  |  | 							avatar_upload_body.append( | 
					
						
							|  |  |  | 								"file", | 
					
						
							|  |  |  | 								avatar, | 
					
						
							|  |  |  | 								encodeURIComponent(avatar.name), | 
					
						
							|  |  |  | 							); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							const avatar_path = `/files/users/${user.id}/avatars/${encodeURIComponent(avatar.name)}`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							const avatar_upload_response = await api.fetch(avatar_path, { | 
					
						
							|  |  |  | 								method: "PUT", | 
					
						
							|  |  |  | 								body: avatar_upload_body, | 
					
						
							|  |  |  | 							}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							if (!avatar_upload_response.ok) { | 
					
						
							|  |  |  | 								const error = await avatar_upload_response.json(); | 
					
						
							|  |  |  | 								return alert(error?.error?.message ?? "Unknown error."); | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							updated_user.meta = updated_user.meta ?? {}; | 
					
						
							|  |  |  | 							updated_user.meta.avatar = `//${window.location.host}${avatar_path}`; | 
					
						
							|  |  |  | 							break; | 
					
						
							|  |  |  | 						default: | 
					
						
							|  |  |  | 							const elements = input.name.split("."); | 
					
						
							|  |  |  | 							let current = updated_user; | 
					
						
							|  |  |  | 							for (const element of elements.slice(0, elements.length - 1)) { | 
					
						
							|  |  |  | 								current[element] = current[element] ?? {}; | 
					
						
							|  |  |  | 								current = current[element]; | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							current[elements.slice(elements.length - 1).shift()] = | 
					
						
							|  |  |  | 								input.value.trim(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							break; | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					const saved_user_response = await api.fetch(`/api/users/${user.id}`, { | 
					
						
							|  |  |  | 						method: "PUT", | 
					
						
							|  |  |  | 						json: updated_user, | 
					
						
							|  |  |  | 					}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					if (!saved_user_response.ok) { | 
					
						
							|  |  |  | 						const error = await avatar_upload_response.json(); | 
					
						
							|  |  |  | 						return alert(error?.error?.message ?? "Unknown error."); | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					const saved_user = await saved_user_response.json(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					document.body.dataset.user = JSON.stringify(saved_user); | 
					
						
							|  |  |  | 					document.body.dataset.perms = saved_user.permissions.join(":"); | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				for (const input of inputs) { | 
					
						
							|  |  |  | 					function on_updated(event) { | 
					
						
							|  |  |  | 						if (input.__debounce_timeout) { | 
					
						
							|  |  |  | 							clearTimeout(input.__debounce_timeout); | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						input.__debounce_timeout = setTimeout(() => { | 
					
						
							|  |  |  | 							update_from_input(input); | 
					
						
							|  |  |  | 						}, 250); | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					input.addEventListener("input", on_updated); | 
					
						
							|  |  |  | 					input.addEventListener("change", on_updated); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			}); | 
					
						
							|  |  |  | 		</script> | 
					
						
							|  |  |  | 		<div class="avatar-container"> | 
					
						
							|  |  |  | 			<img | 
					
						
							|  |  |  | 				id="user-avatar" | 
					
						
							|  |  |  | 				src="/images/default_avatar.gif" | 
					
						
							|  |  |  | 				alt="User Avatar" | 
					
						
							|  |  |  | 				data-bind-to-user-field="meta.avatar" | 
					
						
							|  |  |  | 				data-user-field-target="src" | 
					
						
							|  |  |  | 				data-user-field-default="/images/default_avatar.gif" | 
					
						
							|  |  |  | 			/> | 
					
						
							|  |  |  | 			<input type="file" accept="image/*" name="meta.avatar" /> | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 		</div> | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-16 19:48:01 -07:00
										 |  |  | 		<details class="additional-profile"> | 
					
						
							|  |  |  | 			<summary> | 
					
						
							|  |  |  | 				<div class="username-container"> | 
					
						
							|  |  |  | 					<span class="username" data-bind-to-user-field="username"></span> | 
					
						
							|  |  |  | 				</div> | 
					
						
							|  |  |  | 			</summary> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			<div class="notifications-settings-container"> | 
					
						
							|  |  |  | 				<button class="mockup" onclick="NOTIFICATIONS.request_permission()"> | 
					
						
							|  |  |  | 					Enable Notifications | 
					
						
							|  |  |  | 				</button> | 
					
						
							|  |  |  | 			</div> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			<div class="color-settings-container"> | 
					
						
							|  |  |  | 				<input | 
					
						
							|  |  |  | 					type="text" | 
					
						
							|  |  |  | 					id="user-color-setting-primary" | 
					
						
							|  |  |  | 					name="meta.primary_color" | 
					
						
							|  |  |  | 					value="" | 
					
						
							|  |  |  | 					data-bind-to-user-field="meta.primary_color" | 
					
						
							|  |  |  | 					data-user-field-target="value" | 
					
						
							|  |  |  | 					data-user-field-default="" | 
					
						
							|  |  |  | 				/> | 
					
						
							| 
									
										
										
										
											2025-09-16 20:11:42 -07:00
										 |  |  | 				<label class="placeholder" for="user-color-setting-primary">Primary Color</label> | 
					
						
							| 
									
										
										
										
											2025-09-16 19:48:01 -07:00
										 |  |  | 			</div> | 
					
						
							|  |  |  | 		</details> | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 	</form> | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-10-08 17:38:23 -07:00
										 |  |  | 	<button | 
					
						
							|  |  |  | 		style="text-transform: uppercase; width: 100%; padding: 1.1rem 0" | 
					
						
							|  |  |  | 		onclick="generate_invite(event)" | 
					
						
							|  |  |  | 	> | 
					
						
							|  |  |  | 		Invite Another Human | 
					
						
							|  |  |  | 	</button> | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-16 19:48:01 -07:00
										 |  |  | 	<form | 
					
						
							|  |  |  | 		data-smart="true" | 
					
						
							|  |  |  | 		data-method="DELETE" | 
					
						
							|  |  |  | 		action="/api/auth" | 
					
						
							|  |  |  | 		style="position: absolute; left: 1rem; right: 1rem; bottom: 1rem" | 
					
						
							|  |  |  | 	> | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 		<script> | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				const form = document.currentScript.closest("form"); | 
					
						
							|  |  |  | 				form.on_reply = (response) => { | 
					
						
							|  |  |  | 					if (!response.deleted) { | 
					
						
							|  |  |  | 						alert("error logging out? please reload."); | 
					
						
							|  |  |  | 						return; | 
					
						
							|  |  |  | 					} | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 					delete document.body.dataset.user; | 
					
						
							|  |  |  | 					delete document.body.dataset.perms; | 
					
						
							|  |  |  | 					window.location = "/"; | 
					
						
							| 
									
										
										
										
											2025-09-12 11:41:15 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-15 12:02:03 -07:00
										 |  |  | 					document.dispatchEvent(new CustomEvent("user_logged_out", { detail: {} })); | 
					
						
							|  |  |  | 				}; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		</script> | 
					
						
							|  |  |  | 		<button class="primary">Log Out</button> | 
					
						
							|  |  |  | 	</form> | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	<div class="topics-container"> | 
					
						
							| 
									
										
										
										
											2025-09-16 19:54:43 -07:00
										 |  |  | 		<div style="margin-bottom: 1rem"> | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 			<span class="title">topics</span> | 
					
						
							|  |  |  | 		</div> | 
					
						
							|  |  |  | 		<ul id="topic-list" class="topic-list"></ul> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		<div id="topic-creation-container" data-requires-permission="topics.create"> | 
					
						
							|  |  |  | 			<button | 
					
						
							|  |  |  | 				id="toggle-topic-creation-form-button" | 
					
						
							|  |  |  | 				onclick="((event) => { | 
					
						
							|  |  |  | 					event.preventDefault(); | 
					
						
							|  |  |  | 				 	const topic_create_form = document.getElementById( 'topic-create' ); | 
					
						
							|  |  |  | 					topic_create_form.style[ 'height' ] = topic_create_form.style[ 'height' ] === '5rem' ? '0' : '5rem'; | 
					
						
							|  |  |  | 				})(event)" | 
					
						
							|  |  |  | 			> | 
					
						
							|  |  |  | 				<div class="icon plus"></div> | 
					
						
							|  |  |  | 			</button> | 
					
						
							|  |  |  | 			<form | 
					
						
							|  |  |  | 				id="topic-create" | 
					
						
							|  |  |  | 				data-smart="true" | 
					
						
							|  |  |  | 				action="/api/topics" | 
					
						
							|  |  |  | 				method="POST" | 
					
						
							|  |  |  | 				style=" | 
					
						
							|  |  |  | 					margin-top: 1rem; | 
					
						
							|  |  |  | 					width: 100%; | 
					
						
							|  |  |  | 					overflow: hidden; | 
					
						
							|  |  |  | 					height: 0; | 
					
						
							|  |  |  | 					overflow: hidden; | 
					
						
							|  |  |  | 					transition: all 0.5s; | 
					
						
							|  |  |  | 				" | 
					
						
							|  |  |  | 			> | 
					
						
							|  |  |  | 				<input | 
					
						
							|  |  |  | 					id="new-topic-name-input" | 
					
						
							|  |  |  | 					type="text" | 
					
						
							|  |  |  | 					name="name" | 
					
						
							|  |  |  | 					value="" | 
					
						
							|  |  |  | 					placeholder="new topic" | 
					
						
							|  |  |  | 				/> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				<input type="submit" hidden /> | 
					
						
							|  |  |  | 				<script> | 
					
						
							|  |  |  | 					{ | 
					
						
							|  |  |  | 						const form = document.currentScript.closest("form"); | 
					
						
							|  |  |  | 						const topic_create_form = document.getElementById("topic-create"); | 
					
						
							|  |  |  | 						const new_topic_name_input = | 
					
						
							|  |  |  | 							document.getElementById("new-topic-name-input"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						form.on_reply = (new_topic) => { | 
					
						
							|  |  |  | 							const topic_list = document.getElementById("topic-list"); | 
					
						
							|  |  |  | 							topic_list.insertAdjacentHTML( | 
					
						
							|  |  |  | 								"beforeend", | 
					
						
							|  |  |  | 								`<li id="topic-selector-${new_topic.id}" class="topic"><a href="#/topic/${new_topic.id}">${new_topic.name}</a></li>`, | 
					
						
							|  |  |  | 							); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							new_topic_name_input.value = ""; | 
					
						
							| 
									
										
										
										
											2025-10-10 16:39:44 -07:00
										 |  |  | 							window.location.hash = `/topic/${new_topic.id}/chat`; | 
					
						
							| 
									
										
										
										
											2025-09-11 14:09:28 -07:00
										 |  |  | 							topic_create_form.style["height"] = "0"; | 
					
						
							|  |  |  | 						}; | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 				</script> | 
					
						
							|  |  |  | 			</form> | 
					
						
							|  |  |  | 		</div> | 
					
						
							|  |  |  | 	</div> | 
					
						
							| 
									
										
										
										
											2025-09-10 19:38:38 -07:00
										 |  |  | </div> |