| 
							
								 | 
							
							// Hello reader!
 | 
						
						
						
						
							 | 
							
								 | 
							
							// This project can be found at:
 | 
						
						
						
						
							 | 
							
								 | 
							
							// https://codeberg.com/gravel/sessioncommunities.online
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * This JavaScript file uses the JSDoc commenting style.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Learn more: https://jsdoc.app/
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							// Nudge TypeScript plugins to type-check using JSDoc comments.
 | 
						
						
						
						
							 | 
							
								 | 
							
							// @ts-check
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							// Early prevention for bugs introduced by lazy coding.
 | 
						
						
						
						
							 | 
							
								 | 
							
							'use strict';
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							// Import magic numbers and data
 | 
						
						
						
						
							 | 
							
								 | 
							
							import {
 | 
						
						
						
						
							 | 
							
								 | 
							
								dom, COLUMN, COLUMN_LITERAL, COMPARISON, ATTRIBUTES,
 | 
						
						
						
						
							 | 
							
								 | 
							
								columnAscendingByDefault, columnIsSortable, COLUMN_TRANSFORMATION,
 | 
						
						
						
						
							 | 
							
								 | 
							
								element, JOIN_URL_PASTE, communityQRCodeURL, STAFF_ID_PASTE, IDENTIFIER_PASTE, DETAILS_LINK_PASTE
 | 
						
						
						
						
							 | 
							
								 | 
							
							} from './js/constants.js';
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							// Hidden communities for transparency.
 | 
						
						
						
						
							 | 
							
								 | 
							
							const filteredCommunities = {
 | 
						
						
						
						
							 | 
							
								 | 
							
								tests: [
 | 
						
						
						
						
							 | 
							
								 | 
							
									"fishing+8e2e", // Example group from PySOGS documentation
 | 
						
						
						
						
							 | 
							
								 | 
							
									"test+118d",    // Testing 1, 2, 3
 | 
						
						
						
						
							 | 
							
								 | 
							
									"test+13f6",    // Testing room2
 | 
						
						
						
						
							 | 
							
								 | 
							
									"test+fe93",    // 测试(Test)
 | 
						
						
						
						
							 | 
							
								 | 
							
									"xyz+7908",     // XYZ Room
 | 
						
						
						
						
							 | 
							
								 | 
							
								],
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								offensive: [
 | 
						
						
						
						
							 | 
							
								 | 
							
									"aiunlimited+fc30", // illegal material
 | 
						
						
						
						
							 | 
							
								 | 
							
									"AlexMed+e093",     // drug trading?
 | 
						
						
						
						
							 | 
							
								 | 
							
									"gore+e5e0",        // illegal material
 | 
						
						
						
						
							 | 
							
								 | 
							
									"internet+70d0",    // illegal activity
 | 
						
						
						
						
							 | 
							
								 | 
							
									"k9training+fdcb",  // illegal material
 | 
						
						
						
						
							 | 
							
								 | 
							
									"dogmen+fdcb",      // illegal material
 | 
						
						
						
						
							 | 
							
								 | 
							
									"RU-STEROID+e093", 	// drug trading?
 | 
						
						
						
						
							 | 
							
								 | 
							
									"thestart+e4b1",    // drug trading
 | 
						
						
						
						
							 | 
							
								 | 
							
									"deutschclub+e4b1", // drug trading?
 | 
						
						
						
						
							 | 
							
								 | 
							
									"cocaine+e4b1",     // drug trading
 | 
						
						
						
						
							 | 
							
								 | 
							
									"chigua+4567",      // illegal material
 | 
						
						
						
						
							 | 
							
								 | 
							
									"A4hanguo+4567",    // illegal material
 | 
						
						
						
						
							 | 
							
								 | 
							
								],
 | 
						
						
						
						
							 | 
							
								 | 
							
							};
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Hanging reference to preloaded images to avoid garbage collection.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							let preloadedImages = [];
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Create an interactive version of the Community join link.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {string} join_link
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @returns {HTMLElement}
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							const transformJoinURL = (join_link) => {
 | 
						
						
						
						
							 | 
							
								 | 
							
								return element.button({
 | 
						
						
						
						
							 | 
							
								 | 
							
									textContent: "Copy",
 | 
						
						
						
						
							 | 
							
								 | 
							
									className: "copy_button",
 | 
						
						
						
						
							 | 
							
								 | 
							
									title: "Click here to copy the join URL",
 | 
						
						
						
						
							 | 
							
								 | 
							
									onclick: () => copyToClipboard(join_link)
 | 
						
						
						
						
							 | 
							
								 | 
							
								});
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Fetches the last modification timestamp from the DOM.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @returns {?number}
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function getTimestamp() {
 | 
						
						
						
						
							 | 
							
								 | 
							
								const timestampRaw = dom.meta_timestamp()
 | 
						
						
						
						
							 | 
							
								 | 
							
									?.getAttribute('content');
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (!timestampRaw) return null;
 | 
						
						
						
						
							 | 
							
								 | 
							
								const timestamp = parseInt(timestampRaw);
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (Number.isNaN(timestamp)) return null;
 | 
						
						
						
						
							 | 
							
								 | 
							
								return timestamp;
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Processes initial URL hash and parameter to trigger actions on the page.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function reactToURLParameters() {
 | 
						
						
						
						
							 | 
							
								 | 
							
								const hash = location.hash;
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (hash == "") return;
 | 
						
						
						
						
							 | 
							
								 | 
							
								const communityID = hash.slice(1);
 | 
						
						
						
						
							 | 
							
								 | 
							
								const row = dom.community_row(communityID);
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (row == null || !(row instanceof HTMLTableRowElement)) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									return;
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								// manual scrolling to prevent jumping after every modal open
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								row.scrollIntoView({
 | 
						
						
						
						
							 | 
							
								 | 
							
									behavior: "smooth"
 | 
						
						
						
						
							 | 
							
								 | 
							
								});
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								try {
 | 
						
						
						
						
							 | 
							
								 | 
							
									displayQRModal(communityID);
 | 
						
						
						
						
							 | 
							
								 | 
							
								} catch (e) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									console.error("Could not navigate to community " + communityID);
 | 
						
						
						
						
							 | 
							
								 | 
							
									console.error(e);
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							function addInformativeInteractions() {
 | 
						
						
						
						
							 | 
							
								 | 
							
								const moreSitesInfoButton = document.getElementById('more-sites-info-button');
 | 
						
						
						
						
							 | 
							
								 | 
							
								moreSitesInfoButton?.addEventListener('click', () => {
 | 
						
						
						
						
							 | 
							
								 | 
							
									alert(
 | 
						
						
						
						
							 | 
							
								 | 
							
										`Lokinet Gitea and session.directory compile lists of
 | 
						
						
						
						
							 | 
							
								 | 
							
										Session Closed Groups and Communities, and are linked
 | 
						
						
						
						
							 | 
							
								 | 
							
										in recognition of their importance.
 | 
						
						
						
						
							 | 
							
								 | 
							
										However, sessioncommunities.online already includes Communities
 | 
						
						
						
						
							 | 
							
								 | 
							
										from these sources on this page.
 | 
						
						
						
						
							 | 
							
								 | 
							
										`.replace(/\s+/g, " ").trim()
 | 
						
						
						
						
							 | 
							
								 | 
							
									);
 | 
						
						
						
						
							 | 
							
								 | 
							
								});
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Triggers all actions dependent on page load.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function onLoad() {
 | 
						
						
						
						
							 | 
							
								 | 
							
								const timestamp = getTimestamp();
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (timestamp !== null) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									setLastChecked(timestamp);
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
								hideBadCommunities();
 | 
						
						
						
						
							 | 
							
								 | 
							
								// Sort by server to show off new feature & align colors.
 | 
						
						
						
						
							 | 
							
								 | 
							
								sortTable(COLUMN.SERVER_ICON);
 | 
						
						
						
						
							 | 
							
								 | 
							
								createJoinLinkButtons();
 | 
						
						
						
						
							 | 
							
								 | 
							
								markSortableColumns();
 | 
						
						
						
						
							 | 
							
								 | 
							
								addQRModalHandlers();
 | 
						
						
						
						
							 | 
							
								 | 
							
								addServerIconInteractions();
 | 
						
						
						
						
							 | 
							
								 | 
							
								preloadImages();
 | 
						
						
						
						
							 | 
							
								 | 
							
								setInterval(() => {
 | 
						
						
						
						
							 | 
							
								 | 
							
									preloadImages();
 | 
						
						
						
						
							 | 
							
								 | 
							
								}, 60 * 60E3);
 | 
						
						
						
						
							 | 
							
								 | 
							
								reactToURLParameters();
 | 
						
						
						
						
							 | 
							
								 | 
							
								addInformativeInteractions();
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Construct room tag DOM from its description.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {Object} param0
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {string} param0.text Tag name
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {"user"|"reserved"} param0.type Tag classification
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {string} param0.description Tag details
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @returns HTMLElement
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							const tagBody = ({text, type, description}) => element.span({
 | 
						
						
						
						
							 | 
							
								 | 
							
								// todo: truncate
 | 
						
						
						
						
							 | 
							
								 | 
							
								textContent: text,
 | 
						
						
						
						
							 | 
							
								 | 
							
								className: `room-label room-label-${type} badge`,
 | 
						
						
						
						
							 | 
							
								 | 
							
								title: description
 | 
						
						
						
						
							 | 
							
								 | 
							
							});
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Shows the details modal hydrated with the given community's details.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {string} communityID
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {number} pane Pane number to display in modal
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function displayQRModal(communityID, pane = 0) {
 | 
						
						
						
						
							 | 
							
								 | 
							
								const modal = dom.details_modal();
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (!modal) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									throw new DOMException("Modal element not found.");
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								const row = dom.community_row(communityID);
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (!row) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									throw new DOMException("Community row not found.");
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								const rowInfo = dom.row_info(row);
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								for (const element of modal.querySelectorAll(`[${ATTRIBUTES.HYDRATION.CONTENT}]`)) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									const attributes = element.getAttribute(ATTRIBUTES.HYDRATION.CONTENT);
 | 
						
						
						
						
							 | 
							
								 | 
							
									if (!attributes) continue;
 | 
						
						
						
						
							 | 
							
								 | 
							
									for (const attribute of attributes.split(';')) {
 | 
						
						
						
						
							 | 
							
								 | 
							
										const [property, targetProperty] = attribute.includes(':')
 | 
						
						
						
						
							 | 
							
								 | 
							
											? attribute.split(":")
 | 
						
						
						
						
							 | 
							
								 | 
							
											: [attribute, 'textContent'];
 | 
						
						
						
						
							 | 
							
								 | 
							
										if (!Object.getOwnPropertyNames(rowInfo).includes(property)) {
 | 
						
						
						
						
							 | 
							
								 | 
							
											console.error(`Unknown rowInfo property: ${property}`);
 | 
						
						
						
						
							 | 
							
								 | 
							
											continue;
 | 
						
						
						
						
							 | 
							
								 | 
							
										}
 | 
						
						
						
						
							 | 
							
								 | 
							
										if (targetProperty === 'textContent') {
 | 
						
						
						
						
							 | 
							
								 | 
							
											element.textContent = rowInfo[property];
 | 
						
						
						
						
							 | 
							
								 | 
							
										} else {
 | 
						
						
						
						
							 | 
							
								 | 
							
											element.setAttribute(targetProperty, rowInfo[property]);
 | 
						
						
						
						
							 | 
							
								 | 
							
										}
 | 
						
						
						
						
							 | 
							
								 | 
							
									}
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								const tagContainer = dom.details_modal_tag_container();
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								tagContainer.innerHTML = "";
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								tagContainer.append(
 | 
						
						
						
						
							 | 
							
								 | 
							
									...JSON.parse(rowInfo.tags).map(tag => tagBody(tag))
 | 
						
						
						
						
							 | 
							
								 | 
							
								);
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								dom.details_modal_qr_code().src = communityQRCodeURL(communityID);
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								document.getElementById('details-modal-panes').setAttribute('data-pane', pane);
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								location.hash=`#${communityID}`;
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								modal.showModal();
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Hides the Community details modal.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function hideQRModal() {
 | 
						
						
						
						
							 | 
							
								 | 
							
								dom.details_modal().close();
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Adds handlers for details modal-related actions.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function addQRModalHandlers() {
 | 
						
						
						
						
							 | 
							
								 | 
							
								const rows = dom.tbl_communities_content_rows();
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (!rows) throw new Error("Rows not found");
 | 
						
						
						
						
							 | 
							
								 | 
							
								for (const row of rows) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									const communityID = row.getAttribute(ATTRIBUTES.ROW.IDENTIFIER);
 | 
						
						
						
						
							 | 
							
								 | 
							
									for (const cell of ['.td_qr_code', '.td_description', '.td_language', '.td_users']) {
 | 
						
						
						
						
							 | 
							
								 | 
							
										row.querySelector(cell).addEventListener(
 | 
						
						
						
						
							 | 
							
								 | 
							
											'click',
 | 
						
						
						
						
							 | 
							
								 | 
							
											() => displayQRModal(communityID, cell == '.td_qr_code' ? 1 : 0)
 | 
						
						
						
						
							 | 
							
								 | 
							
										);
 | 
						
						
						
						
							 | 
							
								 | 
							
									}
 | 
						
						
						
						
							 | 
							
								 | 
							
									row.addEventListener(
 | 
						
						
						
						
							 | 
							
								 | 
							
										'click',
 | 
						
						
						
						
							 | 
							
								 | 
							
										(e) => {
 | 
						
						
						
						
							 | 
							
								 | 
							
											if (e.target != row) { return; }
 | 
						
						
						
						
							 | 
							
								 | 
							
											displayQRModal(communityID);
 | 
						
						
						
						
							 | 
							
								 | 
							
										}
 | 
						
						
						
						
							 | 
							
								 | 
							
									)
 | 
						
						
						
						
							 | 
							
								 | 
							
									row.querySelector('.td_name').addEventListener(
 | 
						
						
						
						
							 | 
							
								 | 
							
										'click',
 | 
						
						
						
						
							 | 
							
								 | 
							
										(e) => {
 | 
						
						
						
						
							 | 
							
								 | 
							
											e.preventDefault();
 | 
						
						
						
						
							 | 
							
								 | 
							
											displayQRModal(communityID);
 | 
						
						
						
						
							 | 
							
								 | 
							
										}
 | 
						
						
						
						
							 | 
							
								 | 
							
									);
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
								const closeButton =
 | 
						
						
						
						
							 | 
							
								 | 
							
									dom.details_modal().querySelector('#details-modal-close');
 | 
						
						
						
						
							 | 
							
								 | 
							
								closeButton.addEventListener(
 | 
						
						
						
						
							 | 
							
								 | 
							
									'click',
 | 
						
						
						
						
							 | 
							
								 | 
							
									() => hideQRModal()
 | 
						
						
						
						
							 | 
							
								 | 
							
								);
 | 
						
						
						
						
							 | 
							
								 | 
							
								dom.details_modal().addEventListener('click', function (e) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									if (this == e.target) {
 | 
						
						
						
						
							 | 
							
								 | 
							
										this.close();
 | 
						
						
						
						
							 | 
							
								 | 
							
									}
 | 
						
						
						
						
							 | 
							
								 | 
							
								});
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								for (const button of document.querySelectorAll('.details-modal-pane-button')) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									button.addEventListener(
 | 
						
						
						
						
							 | 
							
								 | 
							
										'click',
 | 
						
						
						
						
							 | 
							
								 | 
							
										function () {
 | 
						
						
						
						
							 | 
							
								 | 
							
											const targetPane = this.getAttribute('data-pane');
 | 
						
						
						
						
							 | 
							
								 | 
							
											document.getElementById('details-modal-panes')?.setAttribute('data-pane', targetPane);
 | 
						
						
						
						
							 | 
							
								 | 
							
										}
 | 
						
						
						
						
							 | 
							
								 | 
							
									)
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								document.querySelector('#details-modal-copy-button').addEventListener(
 | 
						
						
						
						
							 | 
							
								 | 
							
									'click',
 | 
						
						
						
						
							 | 
							
								 | 
							
									function () {
 | 
						
						
						
						
							 | 
							
								 | 
							
										copyToClipboard(this.getAttribute('data-href'));
 | 
						
						
						
						
							 | 
							
								 | 
							
									}
 | 
						
						
						
						
							 | 
							
								 | 
							
								)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								document.querySelector('#details-modal-copy-staff-id')?.addEventListener(
 | 
						
						
						
						
							 | 
							
								 | 
							
									'click',
 | 
						
						
						
						
							 | 
							
								 | 
							
									function () {
 | 
						
						
						
						
							 | 
							
								 | 
							
										/**
 | 
						
						
						
						
							 | 
							
								 | 
							
										 * @type {string[]}
 | 
						
						
						
						
							 | 
							
								 | 
							
										 */
 | 
						
						
						
						
							 | 
							
								 | 
							
										const staff = JSON.parse(this.getAttribute(ATTRIBUTES.ROW.STAFF_DATA));
 | 
						
						
						
						
							 | 
							
								 | 
							
										if (staff.length == 0) {
 | 
						
						
						
						
							 | 
							
								 | 
							
											alert("No public moderators available for this Community.");
 | 
						
						
						
						
							 | 
							
								 | 
							
											return;
 | 
						
						
						
						
							 | 
							
								 | 
							
										}
 | 
						
						
						
						
							 | 
							
								 | 
							
										const staffId = staff[~~(staff.length * Math.random())];
 | 
						
						
						
						
							 | 
							
								 | 
							
										copyToClipboard(`@${staffId}`, STAFF_ID_PASTE);
 | 
						
						
						
						
							 | 
							
								 | 
							
									}
 | 
						
						
						
						
							 | 
							
								 | 
							
								)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								document.querySelector('#details-modal-copy-room-id')?.addEventListener(
 | 
						
						
						
						
							 | 
							
								 | 
							
									'click',
 | 
						
						
						
						
							 | 
							
								 | 
							
									function () {
 | 
						
						
						
						
							 | 
							
								 | 
							
										const identifier = this.getAttribute(ATTRIBUTES.ROW.IDENTIFIER);
 | 
						
						
						
						
							 | 
							
								 | 
							
										copyToClipboard(identifier, IDENTIFIER_PASTE);
 | 
						
						
						
						
							 | 
							
								 | 
							
									}
 | 
						
						
						
						
							 | 
							
								 | 
							
								)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								document.querySelector('#details-modal-copy-room-details-link')?.addEventListener(
 | 
						
						
						
						
							 | 
							
								 | 
							
									'click',
 | 
						
						
						
						
							 | 
							
								 | 
							
									function() {
 | 
						
						
						
						
							 | 
							
								 | 
							
										copyToClipboard(location.href, DETAILS_LINK_PASTE);
 | 
						
						
						
						
							 | 
							
								 | 
							
									}
 | 
						
						
						
						
							 | 
							
								 | 
							
								)
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								for (const anchor of dom.qr_code_buttons()) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									// Disable QR code links
 | 
						
						
						
						
							 | 
							
								 | 
							
									anchor.setAttribute("href", "#");
 | 
						
						
						
						
							 | 
							
								 | 
							
									anchor.removeAttribute("target");
 | 
						
						
						
						
							 | 
							
								 | 
							
									anchor.addEventListener('click', (e) => { e.preventDefault(); return false });
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Prefetches images used in the page to prevent tracking.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function preloadImages() {
 | 
						
						
						
						
							 | 
							
								 | 
							
								const preloadedImagesNew = [];
 | 
						
						
						
						
							 | 
							
								 | 
							
								const rows = dom.tbl_communities_content_rows();
 | 
						
						
						
						
							 | 
							
								 | 
							
								const identifiers = rows.map(
 | 
						
						
						
						
							 | 
							
								 | 
							
									rowElement => rowElement.getAttribute(ATTRIBUTES.ROW.IDENTIFIER)
 | 
						
						
						
						
							 | 
							
								 | 
							
								);
 | 
						
						
						
						
							 | 
							
								 | 
							
								const icons = rows.map(
 | 
						
						
						
						
							 | 
							
								 | 
							
									rowElement => rowElement.getAttribute(ATTRIBUTES.ROW.ROOM_ICON)
 | 
						
						
						
						
							 | 
							
								 | 
							
								)
 | 
						
						
						
						
							 | 
							
								 | 
							
								for (const identifier of identifiers) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									const image = new Image();
 | 
						
						
						
						
							 | 
							
								 | 
							
									image.src = communityQRCodeURL(identifier);
 | 
						
						
						
						
							 | 
							
								 | 
							
									preloadedImages.push(image);
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
								for (const icon of icons) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									if (!icon) {
 | 
						
						
						
						
							 | 
							
								 | 
							
										continue;
 | 
						
						
						
						
							 | 
							
								 | 
							
									}
 | 
						
						
						
						
							 | 
							
								 | 
							
									const image = new Image();
 | 
						
						
						
						
							 | 
							
								 | 
							
									image.src = icon;
 | 
						
						
						
						
							 | 
							
								 | 
							
									preloadedImagesNew.push(image);
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
								preloadedImages = preloadedImagesNew;
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Places join link buttons in the Community rows.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function createJoinLinkButtons() {
 | 
						
						
						
						
							 | 
							
								 | 
							
								const join_URLs = dom.join_urls();
 | 
						
						
						
						
							 | 
							
								 | 
							
								Array.from(join_URLs).forEach((td_url) => {
 | 
						
						
						
						
							 | 
							
								 | 
							
									// Data attributes are more idiomatic and harder to change by accident in the DOM.
 | 
						
						
						
						
							 | 
							
								 | 
							
									const join_link = td_url.getAttribute('data-url');
 | 
						
						
						
						
							 | 
							
								 | 
							
									td_url.append(transformJoinURL(join_link)); // add interactive content
 | 
						
						
						
						
							 | 
							
								 | 
							
								});
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Hides rows of communities deemed to be superfluous or unsuitable.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function hideBadCommunities() {
 | 
						
						
						
						
							 | 
							
								 | 
							
								let numberOfHiddenCommunities = 0;
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								for (const category of ['tests', 'offensive']) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									numberOfHiddenCommunities +=
 | 
						
						
						
						
							 | 
							
								 | 
							
									  filteredCommunities[category]
 | 
						
						
						
						
							 | 
							
								 | 
							
										.map(hideCommunity)
 | 
						
						
						
						
							 | 
							
								 | 
							
										.reduce((a, b) => a + b);
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								const summary = dom.servers_hidden();
 | 
						
						
						
						
							 | 
							
								 | 
							
								summary.innerText = `(${numberOfHiddenCommunities} hidden)`;
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Removes a Community by its ID and returns the number of elements removed.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function hideCommunity(communityID) {
 | 
						
						
						
						
							 | 
							
								 | 
							
								const element = dom.community_row(communityID);
 | 
						
						
						
						
							 | 
							
								 | 
							
								element?.remove();
 | 
						
						
						
						
							 | 
							
								 | 
							
								return element ? 1 : 0;
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Copies text to clipboard and shows an informative toast.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {string} text - Text to copy to clipboard.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {string} [toastText] - Text shown by toast.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function copyToClipboard(text, toastText = JOIN_URL_PASTE) {
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (typeof navigator.clipboard !== "undefined") {
 | 
						
						
						
						
							 | 
							
								 | 
							
									navigator.clipboard.writeText(text);
 | 
						
						
						
						
							 | 
							
								 | 
							
								} else {
 | 
						
						
						
						
							 | 
							
								 | 
							
									toastText = "Can not copy to clipboard in insecure context.";
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								// Find snackbar element
 | 
						
						
						
						
							 | 
							
								 | 
							
								const snackbar = dom.snackbar();
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (!snackbar) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									throw new DOMException("Could not find snackbar");
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								snackbar.textContent = toastText;
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								snackbar.classList.add('show')
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								// After 5 seconds, hide the snackbar.
 | 
						
						
						
						
							 | 
							
								 | 
							
								setTimeout(() => snackbar.classList.remove('show'), 5000);
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Sets the "last checked indicator" based on a timestamp.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {number} last_checked - Timestamp of last community list update.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function setLastChecked(last_checked) {
 | 
						
						
						
						
							 | 
							
								 | 
							
								const seconds_now = Math.floor(Date.now() / 1000); // timestamp in seconds
 | 
						
						
						
						
							 | 
							
								 | 
							
								const time_passed_in_seconds = seconds_now - last_checked;
 | 
						
						
						
						
							 | 
							
								 | 
							
								const time_passed_in_minutes =
 | 
						
						
						
						
							 | 
							
								 | 
							
									Math.floor(time_passed_in_seconds / 60); // time in minutes, rounded down
 | 
						
						
						
						
							 | 
							
								 | 
							
								const timestamp_element = dom.last_checked();
 | 
						
						
						
						
							 | 
							
								 | 
							
								timestamp_element.innerText =	`${time_passed_in_minutes} minutes ago`;
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							// TODO: Move info into dynamic modal.
 | 
						
						
						
						
							 | 
							
								 | 
							
							function addServerIconInteractions() {
 | 
						
						
						
						
							 | 
							
								 | 
							
								const rows = dom.tbl_communities_content_rows();
 | 
						
						
						
						
							 | 
							
								 | 
							
								for (const row of rows) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									const hostname = row.getAttribute(ATTRIBUTES.ROW.HOSTNAME);
 | 
						
						
						
						
							 | 
							
								 | 
							
									const publicKey = row.getAttribute(ATTRIBUTES.ROW.PUBLIC_KEY);
 | 
						
						
						
						
							 | 
							
								 | 
							
									const serverIcon = row.querySelector('.td_server_icon');
 | 
						
						
						
						
							 | 
							
								 | 
							
									serverIcon.addEventListener('click', () => {
 | 
						
						
						
						
							 | 
							
								 | 
							
										alert(`Host: ${hostname}\n\nPublic key:\n${publicKey}`);
 | 
						
						
						
						
							 | 
							
								 | 
							
									});
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Function comparing two elements.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 *
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @callback comparer
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {*} fst - First value to compare.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {*} snd - Second value to compare.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @returns 1 if fst is to come first, -1 if snd is, 0 otherwise.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Performs a comparison on two arbitrary values. Treats "" as Infinity.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {*} fst - First value to compare.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {*} snd - Second value to compare.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @returns 1 if fst > snd, -1 if fst < snd, 0 otherwise.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function compareAscending(fst, snd) {
 | 
						
						
						
						
							 | 
							
								 | 
							
								// Triple equals to avoid "" == 0.
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (fst === "") return COMPARISON.GREATER;
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (snd === "") return COMPARISON.SMALLER;
 | 
						
						
						
						
							 | 
							
								 | 
							
								return (fst > snd) - (fst < snd);
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Performs a comparison on two arbitrary values. Treats "" as Infinity.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {*} fst - First value to compare.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {*} snd - Second value to compare.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @returns -1 if fst > snd, 1 if fst < snd, 0 otherwise.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function compareDescending(fst, snd) {
 | 
						
						
						
						
							 | 
							
								 | 
							
								return -compareAscending(fst, snd);
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Produces a comparer dependent on a derived property of the compared elements.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {comparer} comparer - Callback comparing derived properties.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {Function} getProp - Callback to retrieve derived property.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @returns {comparer} Function comparing elements based on derived property.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function compareProp(comparer, getProp) {
 | 
						
						
						
						
							 | 
							
								 | 
							
								return (fst, snd) => comparer(getProp(fst), getProp(snd));
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Produces a comparer for table rows based on given sorting parameters.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {number} column - Numeric ID of column to be sorted.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {boolean} ascending - Sort ascending if true, descending otherwise.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @returns {comparer}
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function makeRowComparer(column, ascending) {
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (!columnIsSortable(column)) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									throw new Error(`Column ${column} is not sortable`);
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								// Callback to obtain sortable content from cell text.
 | 
						
						
						
						
							 | 
							
								 | 
							
								const columnToSortable = COLUMN_TRANSFORMATION[column] ?? ((el) => el.innerText.trim());
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								// Construct comparer using derived property to determine sort order.
 | 
						
						
						
						
							 | 
							
								 | 
							
								const rowComparer = compareProp(
 | 
						
						
						
						
							 | 
							
								 | 
							
									ascending ? compareAscending : compareDescending,
 | 
						
						
						
						
							 | 
							
								 | 
							
									row => columnToSortable(row.children[column])
 | 
						
						
						
						
							 | 
							
								 | 
							
								);
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								return rowComparer;
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @typedef {Object} SortState
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @property {number} column - Column ID being sorted.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @property {boolean} ascending - Whether the column is sorted ascending.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Retrieves a table's sort settings from the DOM.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {HTMLElement} table - Table of communities being sorted.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @returns {?SortState}
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function getSortState(table) {
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (!table.hasAttribute(ATTRIBUTES.SORTING.ACTIVE)) return null;
 | 
						
						
						
						
							 | 
							
								 | 
							
								const directionState = table.getAttribute(ATTRIBUTES.SORTING.ASCENDING);
 | 
						
						
						
						
							 | 
							
								 | 
							
								// This is not pretty, but the least annoying.
 | 
						
						
						
						
							 | 
							
								 | 
							
								// Checking for classes would be more idiomatic.
 | 
						
						
						
						
							 | 
							
								 | 
							
								const ascending = directionState.toString() === "true";
 | 
						
						
						
						
							 | 
							
								 | 
							
								const columnState = table.getAttribute(ATTRIBUTES.SORTING.COLUMN);
 | 
						
						
						
						
							 | 
							
								 | 
							
								const column = parseInt(columnState);
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (!Number.isInteger(column)) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									throw new Error(`Invalid column number read from table: ${columnState}`)
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
								return { ascending, column };
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Sets a table's sort settings using the DOM.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {HTMLElement} table - Table of communities being sorted.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {SortState} sortState - Sorting settings being applied.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function setSortState(table, { ascending, column }) {
 | 
						
						
						
						
							 | 
							
								 | 
							
								if (!table.hasAttribute(ATTRIBUTES.SORTING.ACTIVE)) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									table.setAttribute(ATTRIBUTES.SORTING.ACTIVE, true);
 | 
						
						
						
						
							 | 
							
								 | 
							
								}
 | 
						
						
						
						
							 | 
							
								 | 
							
								table.setAttribute(ATTRIBUTES.SORTING.ASCENDING, ascending);
 | 
						
						
						
						
							 | 
							
								 | 
							
								table.setAttribute(ATTRIBUTES.SORTING.COLUMN, column);
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
								// No way around this for brief CSS.
 | 
						
						
						
						
							 | 
							
								 | 
							
								const headers = table.querySelectorAll("th");
 | 
						
						
						
						
							 | 
							
								 | 
							
								headers.forEach((th, colno) => {
 | 
						
						
						
						
							 | 
							
								 | 
							
									th.removeAttribute(ATTRIBUTES.SORTING.ACTIVE);
 | 
						
						
						
						
							 | 
							
								 | 
							
								});
 | 
						
						
						
						
							 | 
							
								 | 
							
								headers[column].setAttribute(ATTRIBUTES.SORTING.ACTIVE, true);
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							// This is best done in JS, as it would require <noscript> styles otherwise.
 | 
						
						
						
						
							 | 
							
								 | 
							
							function markSortableColumns() {
 | 
						
						
						
						
							 | 
							
								 | 
							
								const table = dom.tbl_communities();
 | 
						
						
						
						
							 | 
							
								 | 
							
								const header_cells = table.querySelectorAll('th');
 | 
						
						
						
						
							 | 
							
								 | 
							
								for (let colno = 0; colno < header_cells.length; colno++) {
 | 
						
						
						
						
							 | 
							
								 | 
							
									if (!columnIsSortable(colno)) continue;
 | 
						
						
						
						
							 | 
							
								 | 
							
									header_cells[colno].classList.add('sortable');
 | 
						
						
						
						
							 | 
							
								 | 
							
									header_cells[colno].addEventListener(
 | 
						
						
						
						
							 | 
							
								 | 
							
										'click',
 | 
						
						
						
						
							 | 
							
								 | 
							
										() => sortTable(colno)
 | 
						
						
						
						
							 | 
							
								 | 
							
									)
 | 
						
						
						
						
							 | 
							
								 | 
							
								};
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							/**
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Sorts the default communities table according the given column.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * Sort direction is determined by defaults; successive sorts
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * on the same column reverse the sort direction.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 * @param {number} column - Numeric ID of column being sorted.
 | 
						
						
						
						
							 | 
							
								 | 
							
							 */
 | 
						
						
						
						
							 | 
							
								 | 
							
							function sortTable(column) {
 | 
						
						
						
						
							 | 
							
								 | 
							
								const table = dom.tbl_communities();
 | 
						
						
						
						
							 | 
							
								 | 
							
								const sortState = getSortState(table);
 | 
						
						
						
						
							 | 
							
								 | 
							
								const sortingNewColumn = column !== sortState?.column;
 | 
						
						
						
						
							 | 
							
								 | 
							
								const ascending = sortingNewColumn
 | 
						
						
						
						
							 | 
							
								 | 
							
									? columnAscendingByDefault(column)
 | 
						
						
						
						
							 | 
							
								 | 
							
									: !sortState.ascending;
 | 
						
						
						
						
							 | 
							
								 | 
							
								const compare = makeRowComparer(column, ascending);
 | 
						
						
						
						
							 | 
							
								 | 
							
								const rows = Array.from(table.rows).slice(1);
 | 
						
						
						
						
							 | 
							
								 | 
							
								rows.sort(compare);
 | 
						
						
						
						
							 | 
							
								 | 
							
								rows.forEach((row) => row.remove());
 | 
						
						
						
						
							 | 
							
								 | 
							
								table.querySelector("tbody").append(...rows);
 | 
						
						
						
						
							 | 
							
								 | 
							
								setSortState(table, { ascending, column });
 | 
						
						
						
						
							 | 
							
								 | 
							
							}
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							// `html.js` selector for styling purposes
 | 
						
						
						
						
							 | 
							
								 | 
							
							document.documentElement.classList.add("js");
 | 
						
						
						
						
							 | 
							
								 | 
							
							
 | 
						
						
						
						
							 | 
							
								 | 
							
							document.addEventListener('DOMContentLoaded', () => onLoad());
 |