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);
}
static getRoomToken(identifier) {
return identifier.split("+")[0];
}
static getRoomServerId(identifier) {
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 = {
@ -104,20 +139,18 @@ export const dom = {
* @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,
language_flag: RoomInfo.getRoomLanguageFlag(identifier),
name: RoomInfo.getRoomName(identifier),
description: RoomInfo.getRoomDescription(identifier),
users: RoomInfo.getRoomUserCount(identifier),
preview_link: RoomInfo.getRoomPreviewLink(identifier),
join_link: RoomInfo.getRoomJoinLink(identifier),
identifier,
hostname: `${joinURL.protocol}//${joinURL.host}`,
hostname: RoomInfo.getRoomHostname(identifier),
public_key: RoomInfo.getRoomPublicKey(identifier),
staff: RoomInfo.getRoomStaff(identifier),
tags: RoomInfo.getRoomTags(identifier),
@ -251,28 +284,15 @@ export function columnIsSortable(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>}
*/
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
[COLUMN.USERS]: (identifier) => RoomInfo.getRoomUserCount(identifier),
[COLUMN.IDENTIFIER]: (identifier) => identifier.toLowerCase(),
[COLUMN.NAME]: (identifier) => RoomInfo.getRoomName(identifier).toLowerCase(),
[COLUMN.DESCRIPTION]: (identifier) => RoomInfo.getRoomName(identifier).toLowerCase(),
[COLUMN.SERVER_ICON]: (identifier) => RoomInfo.getRoomServerId(identifier)
}
/**
@ -300,3 +320,7 @@ export const element = new Proxy({}, {
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 {
dom, COLUMN, COLUMN_LITERAL, COMPARISON, ATTRIBUTES,
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';
// Hidden communities for transparency.
@ -539,7 +539,9 @@ function addSearchInteractions() {
if (ev.key === "Enter") {
this.blur();
}
useSearchTerm(this.value);
if (this.value === "") {
useSearchTerm("");
}
})
dom.btn_clear_search()?.addEventListener('click', function () {
@ -620,12 +622,12 @@ function makeRowComparer(column, ascending) {
}
// 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.
const rowComparer = compareProp(
ascending ? compareAscending : compareDescending,
row => columnToSortable(row.children[column], row)
({identifier}) => rowToSortable(identifier)
);
return rowComparer;
@ -691,18 +693,25 @@ function markSortableColumns() {
}
/**
* @type {HTMLTableRowElement[]}
* @type {{row: HTMLTableRowElement, identifier: string}[]}
*/
const communityFullRowCache = [];
function getAllCachedRows() {
return communityFullRowCache.map(({row}) => row);
}
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();
if (searchBar === undefined || !(searchBar instanceof HTMLInputElement)) {
@ -710,47 +719,64 @@ function useSearchTerm(rawTerm, fillSearchBarWithTerm = false) {
}
if (!rawTerm) {
replaceRowsWith(communityFullRowCache);
replaceRowsWith(getAllCachedRows());
dom.search_bar()?.classList.remove(CLASSES.SEARCH.NO_RESULTS);
} else {
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 termLanguage = rawTerm.match(/lang:(\S+)/)?.[1];
const newRows = communityFullRowCache.filter(
row => {
const rowInfo = dom.row_info(row);
const langAscii = rowInfo.language_flag && flagToLanguageAscii(rowInfo.language_flag).toLowerCase();
const rowName = rowInfo.name.toLowerCase();
const rowDesc = rowInfo.description.toLowerCase();
const rowTags = rowInfo.tags.map(({text}) => text.replace(/\s+/g, "-"));
if (termLanguage && !langAscii.includes(termLanguage.toLowerCase())) {
return false;
}
if (termTags.length >= 1) {
if (termTags.some(tag => rowTags.some(rowTag => rowTag.startsWith(tag)))) {
/**
* @param {{row: HTMLTableRowElement, identifier: string}} rowCache
*/
async function rowMatches(rowCache) {
const {identifier} = rowCache;
const languageFlag = RoomInfo.getRoomLanguageFlag(identifier);
const langAscii = languageFlag && flagToLanguageAscii(languageFlag).toLowerCase();
if (termLanguage && !langAscii.includes(termLanguage.toLowerCase())) {
return false;
}
const rowName = RoomInfo.getRoomName(identifier).toLowerCase();
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 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) {
searchBar.classList.add(CLASSES.SEARCH.NO_RESULTS);
} else {
searchBar.classList.remove(CLASSES.SEARCH.NO_RESULTS);
}
replaceRowsWith(newRows);
}
if (fillSearchBarWithTerm) {
searchBar.value = rawTerm;
}
sortTable();
}
/**
* @param {HTMLTableRowElement[]} rows
*/
function replaceRowsWith(rows) {
dom.tbl_communities_content_rows().forEach(row => row.remove());
dom.tbl_communities().querySelector("tbody").append(...rows);
const tableBody = dom.tbl_communities()?.querySelector("tbody");
if (!tableBody) throw new Error("Table body missing")
tableBody.replaceChildren(tableBody.rows[0], ...rows);
}
/**
@ -761,21 +787,25 @@ function replaceRowsWith(rows) {
*/
function sortTable(column) {
const table = dom.tbl_communities();
if (!table) throw new Error("Table missing");
const sortState = getSortState(table);
const sortingAsBefore = column === undefined;
if (!sortState && !sortingAsBefore) {
throw new Error("Must supply column on first sort");
}
const sortingNewColumn = column !== sortState?.column;
const sortedColumn = column ?? sortState?.column;
const sortedColumn = column ?? sortState?.column ?? unreachable();
const ascending =
sortingAsBefore ?
sortState.ascending : (
sortState?.ascending ?? unreachable() : (
sortingNewColumn
? columnAscendingByDefault(column)
: !sortState.ascending
: sortState?.ascending ?? unreachable()
);
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);
replaceRowsWith(rows);
replaceRowsWith(rows.map(({row}) => row));
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>
<?php foreach ($TABLE_COLUMNS as $colno => $column): ?>
<th<?=sort_onclick($colno)?> id="th_<?=$column['id']?>" class="tbl_communities__th">

Loading…
Cancel
Save