You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			291 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			291 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
// This file contains definitions which help to reduce the amount
 | 
						|
// of redundant values in the main file, especially those that could
 | 
						|
// change in the foreseeable future.
 | 
						|
 | 
						|
class _RoomInfo {
 | 
						|
	static ROOMS_ENDPOINT = '/servers.json';
 | 
						|
	static rooms = {};
 | 
						|
	static servers = {};
 | 
						|
 | 
						|
	static async fetchRooms() {
 | 
						|
		const response = await fetch(this.ROOMS_ENDPOINT);
 | 
						|
		const servers = await response.json();
 | 
						|
		for (const server of servers) {
 | 
						|
			const { server_id } = server;
 | 
						|
			for (const room of server.rooms) {
 | 
						|
				const identifier = `${room.token}+${server_id}`;
 | 
						|
				this.rooms[identifier] = {...room, server_id};
 | 
						|
			}
 | 
						|
			delete server.rooms;
 | 
						|
			this.servers[server_id] = server;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param {string} identifier
 | 
						|
	 */
 | 
						|
	static assertRoomExists(identifier) {
 | 
						|
		if (!(identifier in this.rooms)) {
 | 
						|
			throw new Error(`No such room: ${identifier}`);
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param {string} identifier
 | 
						|
	 * @returns {CommunityRoom}
 | 
						|
	 */
 | 
						|
	static getRoom(identifier) {
 | 
						|
		this.assertRoomExists(identifier);
 | 
						|
		return this.rooms[identifier];
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param {string} identifier
 | 
						|
	 * @returns {CommunityServer}
 | 
						|
	 */
 | 
						|
	static getRoomServer(identifier) {
 | 
						|
		this.assertRoomExists(identifier);
 | 
						|
		return this.servers[this.rooms[identifier].server_id];
 | 
						|
	}
 | 
						|
}
 | 
						|
export class RoomInfo {
 | 
						|
	static async fetchRooms() {
 | 
						|
		return _RoomInfo.fetchRooms();
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param {string} identifier
 | 
						|
	 * @returns {{type: string, text: string, description: string}[]}
 | 
						|
	 */
 | 
						|
	static getRoomTags(identifier) {
 | 
						|
		return _RoomInfo.getRoom(identifier).tags;
 | 
						|
	}
 | 
						|
 | 
						|
	static getRoomStaff(identifier) {
 | 
						|
		const room = _RoomInfo.getRoom(identifier);
 | 
						|
		const { admins = [], moderators = [] } = room;
 | 
						|
		return [...new Set([...admins, ...moderators])];
 | 
						|
	}
 | 
						|
 | 
						|
	static getRoomPublicKey(identifier) {
 | 
						|
		const server = _RoomInfo.getRoomServer(identifier);
 | 
						|
		return server.pubkey;
 | 
						|
	}
 | 
						|
 | 
						|
	static getRoomCreationDate(identifier) {
 | 
						|
		const room = _RoomInfo.getRoom(identifier);
 | 
						|
		return new Date(room.created * 1000);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export const dom = {
 | 
						|
	/** @return {HTMLTableElement | null} */
 | 
						|
	tbl_communities: () => document.getElementById("tbl_communities"),
 | 
						|
	tbl_communities_content_rows:
 | 
						|
		() => Array.from(dom.tbl_communities()?.rows)?.filter(row => !row.querySelector('th')),
 | 
						|
	community_row: (communityID, matchIdPrefix=false) => {
 | 
						|
		const identifier = ATTRIBUTES.ROW.IDENTIFIER;
 | 
						|
		const matches = matchIdPrefix ? '^=' : '=';
 | 
						|
		return document.querySelector(`.room-row[${identifier}${matches}"${communityID}"]`);
 | 
						|
	},
 | 
						|
	/**
 | 
						|
	 * @param {HTMLTableRowElement} row
 | 
						|
	 */
 | 
						|
	row_info: (row) => {
 | 
						|
		const joinLink = row.querySelector('.td_join_url a[href]').getAttribute('href');
 | 
						|
		const identifier = row.getAttribute(ATTRIBUTES.ROW.IDENTIFIER);
 | 
						|
		const joinURL = new URL(joinLink);
 | 
						|
		const dateCreated = RoomInfo.getRoomCreationDate(identifier);
 | 
						|
		/** @type {string[]} */
 | 
						|
		return {
 | 
						|
			language_flag: row.querySelector('.td_language').textContent.trim(),
 | 
						|
			name: row.querySelector('.td_name-inner').textContent.trim(),
 | 
						|
			description: row.querySelector('.td_description').textContent.trim(),
 | 
						|
			users: parseFloat(row.querySelector('.td_users').textContent.trim()),
 | 
						|
			preview_link: row.querySelector('.td_preview a[href]').getAttribute('href'),
 | 
						|
			join_link: joinLink,
 | 
						|
			identifier,
 | 
						|
			hostname: `${joinURL.protocol}//${joinURL.host}`,
 | 
						|
			public_key: RoomInfo.getRoomPublicKey(identifier),
 | 
						|
			staff: RoomInfo.getRoomStaff(identifier),
 | 
						|
			tags: RoomInfo.getRoomTags(identifier),
 | 
						|
			icon: row.getAttribute(ATTRIBUTES.ROW.ROOM_ICON),
 | 
						|
			has_icon: row.getAttribute(ATTRIBUTES.ROW.ROOM_ICON).trim() != "",
 | 
						|
			icon_safety: row.getAttribute(ATTRIBUTES.ROW.ROOM_ICON_SAFETY),
 | 
						|
			date_created: dateCreated,
 | 
						|
			creation_datestring: dateCreated.toLocaleDateString(undefined, {dateStyle: "medium"})
 | 
						|
		};
 | 
						|
	},
 | 
						|
	meta_timestamp: () => document.querySelector('meta[name=timestamp]'),
 | 
						|
	last_checked: () => document.getElementById("last_checked_value"),
 | 
						|
	/** @return {HTMLDialogElement | null} */
 | 
						|
	details_modal: () => document.getElementById('details-modal'),
 | 
						|
	details_modal_tag_container: () => document.getElementById('details-modal-room-tags'),
 | 
						|
	details_modal_qr_code: () => document.getElementById('details-modal-qr-code'),
 | 
						|
	details_modal_room_icon: () => document.getElementById('details-modal-community-icon'),
 | 
						|
	join_urls: () => document.getElementsByClassName("join_url_container"),
 | 
						|
	servers_hidden: () => document.getElementById("servers_hidden"),
 | 
						|
	snackbar: () => document.getElementById("copy-snackbar"),
 | 
						|
	qr_code_buttons: () => document.querySelectorAll('.qr-code-button'),
 | 
						|
	/** @return {HTMLInputElement | null} */
 | 
						|
	btn_toggle_search: () => document.querySelector('#btn-toggle-search'),
 | 
						|
	/** @return {HTMLInputElement | null} */
 | 
						|
	search_bar: () => document.querySelector('#search-bar'),
 | 
						|
	btn_clear_search: () => document.querySelector("#btn-clear-search"),
 | 
						|
	search_container: () => document.querySelector("#search-container"),
 | 
						|
	sample_searches: () => document.querySelectorAll(".sample-search")
 | 
						|
}
 | 
						|
 | 
						|
export const JOIN_URL_PASTE = "Copied URL to clipboard. Paste into Session app to join";
 | 
						|
 | 
						|
export const STAFF_ID_PASTE = "Copied staff ping to clipboard. Use it in the selected Community to alert a random moderator.";
 | 
						|
 | 
						|
export const IDENTIFIER_PASTE = "Copied internal room identifier. Use it to identify a room, such as when contributing language labels."
 | 
						|
 | 
						|
export const DETAILS_LINK_PASTE = "Copied link to Community details.";
 | 
						|
 | 
						|
export const communityQRCodeURL = (communityID) => `qr-codes/${communityID}.png`
 | 
						|
 | 
						|
export const COLUMN = {
 | 
						|
	LANGUAGE:     0,  NAME:         1,
 | 
						|
	DESCRIPTION:  2,  USERS:        3,  PREVIEW:      4,
 | 
						|
	QR_CODE:      5,  SERVER_ICON:  6,  JOIN_URL:     7
 | 
						|
};
 | 
						|
 | 
						|
// Reverse enum.
 | 
						|
// Takes original key-value pairs, flips them, and casefolds the new values.
 | 
						|
// Should correspond to #th_{} and .td_{} elements in communities table.
 | 
						|
export const COLUMN_LITERAL = Object.fromEntries(
 | 
						|
	Object.entries(COLUMN).map(([name, id]) => [id, name.toLowerCase()])
 | 
						|
);
 | 
						|
 | 
						|
export const COMPARISON = {
 | 
						|
	GREATER: 1, EQUAL: 0, SMALLER: -1
 | 
						|
};
 | 
						|
 | 
						|
export const ATTRIBUTES = {
 | 
						|
	ROW: {
 | 
						|
		TAGS: 'data-tags',
 | 
						|
		IDENTIFIER: 'data-id',
 | 
						|
		PUBLIC_KEY: 'data-pubkey',
 | 
						|
		STAFF_DATA: 'data-staff',
 | 
						|
		ROOM_ICON: 'data-icon',
 | 
						|
		ROOM_ICON_SAFETY: 'data-icon-safe',
 | 
						|
		DATE_CREATED: 'data-created'
 | 
						|
	},
 | 
						|
	SORTING: {
 | 
						|
		ACTIVE: 'data-sort',
 | 
						|
		ASCENDING: 'data-sort-asc',
 | 
						|
		COLUMN: 'data-sorted-by',
 | 
						|
		// COLUMN_LITERAL: 'sorted-by'
 | 
						|
	},
 | 
						|
	HYDRATION: {
 | 
						|
		CONTENT: 'data-hydrate-with'
 | 
						|
	},
 | 
						|
	SEARCH: {
 | 
						|
		TARGET_SEARCH: 'data-search'
 | 
						|
	}
 | 
						|
};
 | 
						|
 | 
						|
export const CLASSES = {
 | 
						|
	COMPONENTS: {
 | 
						|
		COLLAPSED: 'collapsed',
 | 
						|
	},
 | 
						|
	SEARCH: {
 | 
						|
		NO_RESULTS: 'search-no-results',
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
const CODEPOINT_REGIONAL_INDICATOR_A = 0x1F1E6;
 | 
						|
const CODEPOINT_LOWERCASE_A = 0x61;
 | 
						|
 | 
						|
/**
 | 
						|
 *
 | 
						|
 * @param {string} flag
 | 
						|
 */
 | 
						|
export function flagToLanguageAscii(flag) {
 | 
						|
	const regionalIndicators = [0, 2].map(idx => flag.codePointAt(idx));
 | 
						|
	if (regionalIndicators.includes(undefined)) {
 | 
						|
		return "";
 | 
						|
	}
 | 
						|
	const ascii = regionalIndicators
 | 
						|
		.map(codePoint => codePoint - CODEPOINT_REGIONAL_INDICATOR_A)
 | 
						|
		.map(codePoint => codePoint + CODEPOINT_LOWERCASE_A)
 | 
						|
		.map(codePoint => String.fromCodePoint(codePoint))
 | 
						|
		.join("");
 | 
						|
 | 
						|
	switch (ascii) {
 | 
						|
		case "gb":
 | 
						|
			return "en";
 | 
						|
		case "cn":
 | 
						|
			return "zh";
 | 
						|
		default:
 | 
						|
			return ascii;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
export function columnAscendingByDefault(column) {
 | 
						|
	return column != COLUMN.USERS;
 | 
						|
}
 | 
						|
 | 
						|
export function columnIsSortable(column) {
 | 
						|
	return ![
 | 
						|
		COLUMN.QR_CODE,
 | 
						|
		COLUMN.PREVIEW,
 | 
						|
		// Join URL contents are not guaranteed to have visible text.
 | 
						|
		COLUMN.JOIN_URL
 | 
						|
	].includes(column);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @type {Record<string, (el: HTMLTableCellElement, row: HTMLTableRowElement) => any>}
 | 
						|
 */
 | 
						|
const TRANSFORMATION = {
 | 
						|
	numeric: (el) => parseInt(el.innerText),
 | 
						|
	casefold: (el) => el.innerText.toLowerCase().trim(),
 | 
						|
	getName: (_, row) => dom.row_info(row).name.toLowerCase(),
 | 
						|
	getServerId: (_, row) => {
 | 
						|
		const rowInfo = dom.row_info(row);
 | 
						|
		return `${rowInfo.public_key}${rowInfo.join_link}`;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @type {Dictionary<number, (el: HTMLTableCellElement, row: HTMLTableRowElement) => any>}
 | 
						|
 */
 | 
						|
export const COLUMN_TRANSFORMATION = {
 | 
						|
	[COLUMN.USERS]: TRANSFORMATION.numeric,
 | 
						|
	[COLUMN.IDENTIFIER]: TRANSFORMATION.casefold,
 | 
						|
	[COLUMN.NAME]: TRANSFORMATION.getName,
 | 
						|
	[COLUMN.DESCRIPTION]: TRANSFORMATION.casefold,
 | 
						|
	[COLUMN.SERVER_ICON]: TRANSFORMATION.getServerId
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Creates an element, and adds attributes and elements to it.
 | 
						|
 * @param {string} tag - HTML Tag name.
 | 
						|
 * @param {Object|HTMLElement} args - Array of child elements, may start with props.
 | 
						|
 * @returns {HTMLElement}
 | 
						|
 */
 | 
						|
function createElement(tag, ...args) {
 | 
						|
	const element = document.createElement(tag);
 | 
						|
	if (args.length === 0) return element;
 | 
						|
	const propsCandidate = args[0];
 | 
						|
	if (typeof propsCandidate !== "string" && !(propsCandidate instanceof Element)) {
 | 
						|
		// args[0] is not child element or text node
 | 
						|
		// must be props object
 | 
						|
		Object.assign(element, propsCandidate);
 | 
						|
		args.shift();
 | 
						|
	}
 | 
						|
	element.append(...args);
 | 
						|
	return element;
 | 
						|
}
 | 
						|
 | 
						|
export const element = new Proxy({}, {
 | 
						|
	get(_, key) {
 | 
						|
		return (...args) => createElement(key, ...args)
 | 
						|
	}
 | 
						|
});
 | 
						|
 |