diff --git a/output/index.css b/output/index.css index 219fcc0..bcb8513 100644 --- a/output/index.css +++ b/output/index.css @@ -46,10 +46,12 @@ html { font-size: clamp(12px, 2vw, var(--max-font-size-unitless) * 1px); + height: 100%; } body { margin: 0; + height: 100%; } #toggle-theme-switch { @@ -57,8 +59,10 @@ body { } #theming-root { + display: flex; + flex-direction: column; width: 100%; - height: 100%; + min-height: 100%; margin: 0; background-color: var(--secondary-color); color: var(--primary-color); @@ -119,7 +123,7 @@ html.js .noscript, .hidden { /* box-shadow: 0.05em 0.05em 0.1em 0 #4444;*/ } -#tbl_communities .room-label, #details-modal .room-label { +#tbl_communities .room-label, #details-modal .room-label, .sample-search { color: black; } @@ -127,11 +131,11 @@ html.js .noscript, .hidden { opacity: 0.75; } -.room-label-user { +.room-label-user, .sample-search-tag { background-color: greenyellow; } -.room-label-reserved { +.room-label-reserved, .sample-search-tag-reserved { background-color: yellow; } @@ -139,6 +143,14 @@ html.js .noscript, .hidden { background-color: pink; } +.sample-search-plain { + background-color: lightgrey; +} + +.sample-search-language { + background-color: whitesmoke; +} + #tbl_communities .room-label:not(.room-label-highlighted) { background-color: transparent; color: var(--primary-color); @@ -167,6 +179,67 @@ header { flex-grow: 1; } +#search-container { + display: flex; + flex-direction: column; + max-height: 10rem; + justify-content: center; + box-sizing: border-box; + overflow: hidden; + transition: max-height 0.5s; +} + +#search-container > * { + margin-bottom: 1rem; +} + +.collapsed { + max-height: 0 !important; +} + +#search-container.collapsed > * { + display: none !important; +} + +#search { + display: flex; + justify-content: center; + align-items: baseline; + margin-inline: 1rem; +} + +#search-bar { + height: 1.25rem; + color: var(--primary-color); + background-color: var(--secondary-color); + border: 1px var(--primary-color) solid; + text-align: right; + padding: 0.1rem 0.25rem; +} + +#search-bar.search-no-results { + color: red; +} + +#search-bar:placeholder-shown ~ .btn-clear-search { + visibility: hidden; +} + +.btn-clear-search { + margin-right: 1rem; + font-size: 1.5rem; + padding: 0.25rem 1rem; +} + +#sample-searches { + display: flex; + flex-direction: row; + flex-wrap: wrap; + margin-inline: 1rem; + justify-content: center; + row-gap: 0.25rem; +} + /* --- Table --- */ #tbl_communities { @@ -398,6 +471,10 @@ a[href^="https:"] .protocol-indicator::after { /* --- Footer --- */ +#footer-divider { + width: 90%; +} + footer { display: flex; flex-direction: column; diff --git a/output/js/constants.js b/output/js/constants.js index c6a3b02..63c0562 100644 --- a/output/js/constants.js +++ b/output/js/constants.js @@ -22,7 +22,8 @@ export const dom = { hostname: row.getAttribute(ATTRIBUTES.ROW.HOSTNAME), public_key: row.getAttribute(ATTRIBUTES.ROW.PUBLIC_KEY), staff: row.getAttribute(ATTRIBUTES.ROW.STAFF_DATA), - tags: row.getAttribute(ATTRIBUTES.ROW.TAGS), + /** @type {{type: string, text: string, description: string}[]} */ + tags: JSON.parse(row.getAttribute(ATTRIBUTES.ROW.TAGS)), 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), @@ -41,6 +42,13 @@ export const dom = { 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"; @@ -89,9 +97,47 @@ export const ATTRIBUTES = { }, 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)); + 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; } diff --git a/output/main.js b/output/main.js index 36835e3..ba44ae7 100644 --- a/output/main.js +++ b/output/main.js @@ -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 + element, JOIN_URL_PASTE, communityQRCodeURL, STAFF_ID_PASTE, IDENTIFIER_PASTE, DETAILS_LINK_PASTE, CLASSES, flagToLanguageAscii } from './js/constants.js'; // Hidden communities for transparency. @@ -129,16 +129,26 @@ function onLoad() { hideBadCommunities(); // Sort by server to show off new feature & align colors. sortTable(COLUMN.SERVER_ICON); + initializeSearch(); createJoinLinkButtons(); markSortableColumns(); addQRModalHandlers(); addServerIconInteractions(); + addSearchInteractions(); preloadImages(); setInterval(() => { preloadImages(); }, 60 * 60E3); reactToURLParameters(); addInformativeInteractions(); + Array.from(document.querySelectorAll('.enter-clicks')).forEach(element => { + // @ts-ignore + element.addEventListener('keydown', (/** @type {KeyboardEvent} */ ev) => { + if (ev.key == "Enter") { + ev.currentTarget.click(); + } + }) + }) } /** @@ -200,7 +210,7 @@ function displayQRModal(communityID, pane = 0) { tagContainer.innerHTML = ""; tagContainer.append( - ...JSON.parse(rowInfo.tags).map(tag => tagBody(tag)) + ...rowInfo.tags.map(tag => tagBody(tag)) ); dom.details_modal_qr_code().src = communityQRCodeURL(communityID); @@ -438,6 +448,46 @@ function addServerIconInteractions() { } } +function addSearchInteractions() { + dom.btn_toggle_search()?.addEventListener('click', function (ev) { + location.hash="#"; + const container = dom.search_container(); + container?.classList.toggle(CLASSES.COMPONENTS.COLLAPSED); + if (!container?.classList.contains(CLASSES.COMPONENTS.COLLAPSED)) { + const searchBar = dom.search_bar(); + searchBar?.focus(); + // Inconsistent; attempt to align search bar to top to make more space for results. + searchBar?.scrollIntoView({ behavior: 'smooth', inline: 'start' }); + } else { + useSearchTerm(""); + } + }) + + dom.search_bar()?.addEventListener('keydown', function () { + setTimeout(() => useSearchTerm(this.value), 0); + }) + + dom.search_bar()?.addEventListener('keyup', function (ev) { + if (ev.key === "Enter") { + this.blur(); + } + useSearchTerm(this.value); + }) + + dom.btn_clear_search()?.addEventListener('click', function () { + useSearchTerm(""); + const searchBar = dom.search_bar(); + searchBar?.focus(); + searchBar.value = ""; + }) + + Array.from(dom.sample_searches()).forEach(button => button.addEventListener('click', function() { + const targetSearch = button.getAttribute(ATTRIBUTES.SEARCH.TARGET_SEARCH); + useSearchTerm(targetSearch); + dom.search_bar().value = targetSearch; + })) +} + /** * Function comparing two elements. * @@ -562,25 +612,84 @@ function markSortableColumns() { }; } +/** + * @type {HTMLTableRowElement[]} + */ +const communityFullRowCache = []; + +function initializeSearch() { + communityFullRowCache.push(...dom.tbl_communities_content_rows()); +} +/** + * + * @param {string} [rawTerm] + */ +function useSearchTerm(rawTerm) { + if (!rawTerm) { + replaceRowsWith(communityFullRowCache); + 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.includes(tag)))) { + return true; + } + } + return rowName.includes(term) || rowDesc.includes(term); + + } + ); + if (newRows.length === 0) { + dom.search_bar()?.classList.add(CLASSES.SEARCH.NO_RESULTS); + } else { + dom.search_bar()?.classList.remove(CLASSES.SEARCH.NO_RESULTS); + } + replaceRowsWith(newRows); + } + sortTable(); +} + +function replaceRowsWith(rows) { + dom.tbl_communities_content_rows().forEach(row => row.remove()); + dom.tbl_communities().querySelector("tbody").append(...rows); +} + /** * 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. + * @param {number} [column] - Numeric ID of column being sorted. Re-applies last sort if absent. */ function sortTable(column) { const table = dom.tbl_communities(); const sortState = getSortState(table); + const sortingAsBefore = column === undefined; const sortingNewColumn = column !== sortState?.column; - const ascending = sortingNewColumn - ? columnAscendingByDefault(column) - : !sortState.ascending; - const compare = makeRowComparer(column, ascending); + const sortedColumn = column ?? sortState?.column; + const ascending = + sortingAsBefore ? + sortState.ascending : ( + sortingNewColumn + ? columnAscendingByDefault(column) + : !sortState.ascending + ); + const compare = makeRowComparer(sortedColumn, 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 }); + replaceRowsWith(rows); + setSortState(table, { ascending, column: sortedColumn }); } // `html.js` selector for styling purposes diff --git a/sites/+components/communities-search.php b/sites/+components/communities-search.php new file mode 100644 index 0000000..3be0d1f --- /dev/null +++ b/sites/+components/communities-search.php @@ -0,0 +1,44 @@ + "tag-reserved", "text" => "#$tag", "search" => "#$search"); + } + foreach ($sample_searches_tags as $tag) { + $search = str_replace(" ", "-", $tag); + $sample_searches[] = array("type" => "tag", "text" => "#$tag", "search" => "#$search"); + } + foreach ($sample_searches_plain as $text) { + $sample_searches[] = array("type" => "plain", "text" => $text, "search" => $text); + } + foreach ($sample_searches_languages as $language_array) { + $sample_searches[] = array("type" => "language", "text" => $language_array[1], "search" => "lang:" . $language_array[0]); + } +?> + + diff --git a/sites/index.php b/sites/index.php index 98c0389..a005139 100644 --- a/sites/index.php +++ b/sites/index.php @@ -72,10 +72,16 @@
About + Search More groups @@ -97,11 +103,16 @@

Session Communities

+ + + -
+ + +