Improve search & sort performance

dev
gravel 6 months ago
parent 9ab954326a
commit 1ee166782f
Signed by: gravel
GPG Key ID: C0538F3C906B308F

@ -77,9 +77,44 @@ export class RoomInfo {
return new Date(room.created * 1000); return new Date(room.created * 1000);
} }
static getRoomToken(identifier) {
return identifier.split("+")[0];
}
static getRoomServerId(identifier) { static getRoomServerId(identifier) {
return identifier.split("+")[1]; return identifier.split("+")[1];
} }
static getRoomLanguageFlag(identifier) {
return _RoomInfo.getRoom(identifier).language_flag;
}
static getRoomName(identifier) {
return _RoomInfo.getRoom(identifier).name;
}
static getRoomDescription(identifier) {
return _RoomInfo.getRoom(identifier).description;
}
static getRoomUserCount(identifier) {
return _RoomInfo.getRoom(identifier).active_users;
}
static getRoomPreviewLink(identifier) {
const server = _RoomInfo.getRoomServer(identifier);
return `${server.base_url}/r/${RoomInfo.getRoomToken(identifier)}`;
}
static getRoomJoinLink(identifier) {
const server = _RoomInfo.getRoomServer(identifier);
const token = RoomInfo.getRoomToken(identifier);
return `${server.base_url}/${token}?public_key=${server.pubkey}`;
}
static getRoomHostname(identifier) {
return _RoomInfo.getRoomServer(identifier)?.base_url;
}
} }
export const dom = { export const dom = {
@ -104,20 +139,18 @@ export const dom = {
* @param {HTMLTableRowElement} row * @param {HTMLTableRowElement} row
*/ */
row_info: (row) => { row_info: (row) => {
const joinLink = row.querySelector('.td_join_url a[href]').getAttribute('href');
const identifier = row.getAttribute(ATTRIBUTES.ROW.IDENTIFIER); const identifier = row.getAttribute(ATTRIBUTES.ROW.IDENTIFIER);
const joinURL = new URL(joinLink);
const dateCreated = RoomInfo.getRoomCreationDate(identifier); const dateCreated = RoomInfo.getRoomCreationDate(identifier);
/** @type {string[]} */ /** @type {string[]} */
return { return {
language_flag: row.querySelector('.td_language').textContent.trim(), language_flag: RoomInfo.getRoomLanguageFlag(identifier),
name: row.querySelector('.td_name-inner').textContent.trim(), name: RoomInfo.getRoomName(identifier),
description: row.querySelector('.td_description').textContent.trim(), description: RoomInfo.getRoomDescription(identifier),
users: parseFloat(row.querySelector('.td_users').textContent.trim()), users: RoomInfo.getRoomUserCount(identifier),
preview_link: row.querySelector('.td_preview a[href]').getAttribute('href'), preview_link: RoomInfo.getRoomPreviewLink(identifier),
join_link: joinLink, join_link: RoomInfo.getRoomJoinLink(identifier),
identifier, identifier,
hostname: `${joinURL.protocol}//${joinURL.host}`, hostname: RoomInfo.getRoomHostname(identifier),
public_key: RoomInfo.getRoomPublicKey(identifier), public_key: RoomInfo.getRoomPublicKey(identifier),
staff: RoomInfo.getRoomStaff(identifier), staff: RoomInfo.getRoomStaff(identifier),
tags: RoomInfo.getRoomTags(identifier), tags: RoomInfo.getRoomTags(identifier),
@ -251,28 +284,15 @@ export function columnIsSortable(column) {
].includes(column); ].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 RoomInfo.getRoomServerId(rowInfo.identifier);
}
}
/** /**
* @type {Dictionary<number, (el: HTMLTableCellElement, row: HTMLTableRowElement) => any>} * @type {Dictionary<number, (el: HTMLTableCellElement, row: HTMLTableRowElement) => any>}
*/ */
export const COLUMN_TRANSFORMATION = { export const COLUMN_TRANSFORMATION = {
[COLUMN.USERS]: TRANSFORMATION.numeric, [COLUMN.USERS]: (identifier) => RoomInfo.getRoomUserCount(identifier),
[COLUMN.IDENTIFIER]: TRANSFORMATION.casefold, [COLUMN.IDENTIFIER]: (identifier) => identifier.toLowerCase(),
[COLUMN.NAME]: TRANSFORMATION.getName, [COLUMN.NAME]: (identifier) => RoomInfo.getRoomName(identifier).toLowerCase(),
[COLUMN.DESCRIPTION]: TRANSFORMATION.casefold, [COLUMN.DESCRIPTION]: (identifier) => RoomInfo.getRoomName(identifier).toLowerCase(),
[COLUMN.SERVER_ICON]: TRANSFORMATION.getServerId [COLUMN.SERVER_ICON]: (identifier) => RoomInfo.getRoomServerId(identifier)
} }
/** /**
@ -300,3 +320,7 @@ export const element = new Proxy({}, {
return (...args) => createElement(key, ...args) return (...args) => createElement(key, ...args)
} }
}); });
export const unreachable = () => { throw new Error("Unreachable"); };
export const workOnMainThread = () => new Promise(resolve => setTimeout(resolve, 0));

@ -17,7 +17,7 @@
import { import {
dom, COLUMN, COLUMN_LITERAL, COMPARISON, ATTRIBUTES, dom, COLUMN, COLUMN_LITERAL, COMPARISON, ATTRIBUTES,
columnAscendingByDefault, columnIsSortable, COLUMN_TRANSFORMATION, columnAscendingByDefault, columnIsSortable, COLUMN_TRANSFORMATION,
element, JOIN_URL_PASTE, communityQRCodeURL, STAFF_ID_PASTE, IDENTIFIER_PASTE, DETAILS_LINK_PASTE, CLASSES, flagToLanguageAscii, RoomInfo element, JOIN_URL_PASTE, communityQRCodeURL, STAFF_ID_PASTE, IDENTIFIER_PASTE, DETAILS_LINK_PASTE, CLASSES, flagToLanguageAscii, RoomInfo, unreachable, workOnMainThread
} from './js/util.js'; } from './js/util.js';
// Hidden communities for transparency. // Hidden communities for transparency.
@ -539,7 +539,9 @@ function addSearchInteractions() {
if (ev.key === "Enter") { if (ev.key === "Enter") {
this.blur(); this.blur();
} }
useSearchTerm(this.value); if (this.value === "") {
useSearchTerm("");
}
}) })
dom.btn_clear_search()?.addEventListener('click', function () { dom.btn_clear_search()?.addEventListener('click', function () {
@ -620,12 +622,12 @@ function makeRowComparer(column, ascending) {
} }
// Callback to obtain sortable content from cell text. // Callback to obtain sortable content from cell text.
const columnToSortable = COLUMN_TRANSFORMATION[column] ?? ((el) => el.innerText.trim()); const rowToSortable = COLUMN_TRANSFORMATION[column];
// Construct comparer using derived property to determine sort order. // Construct comparer using derived property to determine sort order.
const rowComparer = compareProp( const rowComparer = compareProp(
ascending ? compareAscending : compareDescending, ascending ? compareAscending : compareDescending,
row => columnToSortable(row.children[column], row) ({identifier}) => rowToSortable(identifier)
); );
return rowComparer; return rowComparer;
@ -691,18 +693,25 @@ function markSortableColumns() {
} }
/** /**
* @type {HTMLTableRowElement[]} * @type {{row: HTMLTableRowElement, identifier: string}[]}
*/ */
const communityFullRowCache = []; const communityFullRowCache = [];
function getAllCachedRows() {
return communityFullRowCache.map(({row}) => row);
}
function initializeSearch() { function initializeSearch() {
communityFullRowCache.push(...dom.tbl_communities_content_rows()); communityFullRowCache.push(...dom.tbl_communities_content_rows().map(row => ({
row,
identifier: row.getAttribute(ATTRIBUTES.ROW.IDENTIFIER) ?? unreachable()
})));
} }
/** /**
* *
* @param {string} [rawTerm] * @param {string} rawTerm
*/ */
function useSearchTerm(rawTerm, fillSearchBarWithTerm = false) { async function useSearchTerm(rawTerm, fillSearchBarWithTerm = false) {
const searchBar = dom.search_bar(); const searchBar = dom.search_bar();
if (searchBar === undefined || !(searchBar instanceof HTMLInputElement)) { if (searchBar === undefined || !(searchBar instanceof HTMLInputElement)) {
@ -710,47 +719,64 @@ function useSearchTerm(rawTerm, fillSearchBarWithTerm = false) {
} }
if (!rawTerm) { if (!rawTerm) {
replaceRowsWith(communityFullRowCache); replaceRowsWith(getAllCachedRows());
dom.search_bar()?.classList.remove(CLASSES.SEARCH.NO_RESULTS); dom.search_bar()?.classList.remove(CLASSES.SEARCH.NO_RESULTS);
} else { } else {
const term = rawTerm.toLowerCase().replace(/lang:(\S+)/g, "").trim(); const term = rawTerm.toLowerCase().replace(/lang:(\S+)/g, "").trim();
const termTags = Array.from(rawTerm.matchAll(/#[^#\s]+/g)).map(match => match[0].slice(1).toLowerCase()); const termTags = Array.from(rawTerm.matchAll(/#[^#\s]+/g)).map(match => match[0].slice(1).toLowerCase());
const termLanguage = rawTerm.match(/lang:(\S+)/)?.[1]; const termLanguage = rawTerm.match(/lang:(\S+)/)?.[1];
const newRows = communityFullRowCache.filter( /**
row => { * @param {{row: HTMLTableRowElement, identifier: string}} rowCache
const rowInfo = dom.row_info(row); */
const langAscii = rowInfo.language_flag && flagToLanguageAscii(rowInfo.language_flag).toLowerCase(); async function rowMatches(rowCache) {
const rowName = rowInfo.name.toLowerCase(); const {identifier} = rowCache;
const rowDesc = rowInfo.description.toLowerCase(); const languageFlag = RoomInfo.getRoomLanguageFlag(identifier);
const rowTags = rowInfo.tags.map(({text}) => text.replace(/\s+/g, "-")); const langAscii = languageFlag && flagToLanguageAscii(languageFlag).toLowerCase();
if (termLanguage && !langAscii.includes(termLanguage.toLowerCase())) { if (termLanguage && !langAscii.includes(termLanguage.toLowerCase())) {
return false; return false;
} }
if (termTags.length >= 1) { const rowName = RoomInfo.getRoomName(identifier).toLowerCase();
if (termTags.some(tag => rowTags.some(rowTag => rowTag.startsWith(tag)))) { const rowDesc = RoomInfo.getRoomDescription(identifier).toLowerCase();
if (rowName.includes(term) || rowDesc.includes(term)) {
return true;
}
const rowTags = RoomInfo.getRoomTags(identifier).map(({text}) => text.replace(/\s+/g, "-"));
for (const termTag of termTags) {
for (const rowTag of rowTags) {
if (rowTag.startsWith(termTag)) {
return true; return true;
} }
} }
return rowName.includes(term) || rowDesc.includes(term);
} }
); return false;
}
console.time("search");
const newRowMatches = communityFullRowCache.map(async (rowCache) => ({ rowCache, doesMatch: await rowMatches(rowCache) }));
const newRows = (await Promise.all(newRowMatches)).filter((row) => row.doesMatch).map(({rowCache}) => rowCache.row);
console.timeEnd("search");
if (newRows.length === 0) { if (newRows.length === 0) {
searchBar.classList.add(CLASSES.SEARCH.NO_RESULTS); searchBar.classList.add(CLASSES.SEARCH.NO_RESULTS);
} else { } else {
searchBar.classList.remove(CLASSES.SEARCH.NO_RESULTS); searchBar.classList.remove(CLASSES.SEARCH.NO_RESULTS);
} }
replaceRowsWith(newRows); replaceRowsWith(newRows);
} }
if (fillSearchBarWithTerm) { if (fillSearchBarWithTerm) {
searchBar.value = rawTerm; searchBar.value = rawTerm;
} }
sortTable();
} }
/**
* @param {HTMLTableRowElement[]} rows
*/
function replaceRowsWith(rows) { function replaceRowsWith(rows) {
dom.tbl_communities_content_rows().forEach(row => row.remove()); const tableBody = dom.tbl_communities()?.querySelector("tbody");
dom.tbl_communities().querySelector("tbody").append(...rows); if (!tableBody) throw new Error("Table body missing")
tableBody.replaceChildren(tableBody.rows[0], ...rows);
} }
/** /**
@ -761,21 +787,25 @@ function replaceRowsWith(rows) {
*/ */
function sortTable(column) { function sortTable(column) {
const table = dom.tbl_communities(); const table = dom.tbl_communities();
if (!table) throw new Error("Table missing");
const sortState = getSortState(table); const sortState = getSortState(table);
const sortingAsBefore = column === undefined; const sortingAsBefore = column === undefined;
if (!sortState && !sortingAsBefore) {
throw new Error("Must supply column on first sort");
}
const sortingNewColumn = column !== sortState?.column; const sortingNewColumn = column !== sortState?.column;
const sortedColumn = column ?? sortState?.column; const sortedColumn = column ?? sortState?.column ?? unreachable();
const ascending = const ascending =
sortingAsBefore ? sortingAsBefore ?
sortState.ascending : ( sortState?.ascending ?? unreachable() : (
sortingNewColumn sortingNewColumn
? columnAscendingByDefault(column) ? columnAscendingByDefault(column)
: !sortState.ascending : sortState?.ascending ?? unreachable()
); );
const compare = makeRowComparer(sortedColumn, ascending); const compare = makeRowComparer(sortedColumn, ascending);
const rows = Array.from(table.rows).slice(1); const rows = dom.tbl_communities_content_rows().map(row => ({row, identifier: row.getAttribute(ATTRIBUTES.ROW.IDENTIFIER)}));
rows.sort(compare); rows.sort(compare);
replaceRowsWith(rows); replaceRowsWith(rows.map(({row}) => row));
setSortState(table, { ascending, column: sortedColumn }); setSortState(table, { ascending, column: sortedColumn });
} }

@ -38,7 +38,7 @@
]; ];
?> ?>
<table id="tbl_communities"> <table id="tbl_communities" data-sort="true" data-sort-asc="true" data-sorted-by="6">
<tr> <tr>
<?php foreach ($TABLE_COLUMNS as $colno => $column): ?> <?php foreach ($TABLE_COLUMNS as $colno => $column): ?>
<th<?=sort_onclick($colno)?> id="th_<?=$column['id']?>" class="tbl_communities__th"> <th<?=sort_onclick($colno)?> id="th_<?=$column['id']?>" class="tbl_communities__th">

Loading…
Cancel
Save