diff --git a/output/js/constants.js b/output/js/constants.js index 49c040a0..2db0fc65 100644 --- a/output/js/constants.js +++ b/output/js/constants.js @@ -5,6 +5,9 @@ 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')), + meta_timestamp: () => document.querySelector('meta[name=timestamp]'), last_checked: () => document.getElementById("last_checked_value"), qr_modal: (communityID) => document.getElementById(`modal_${communityID}`), join_urls: () => document.getElementsByClassName("join_url_container"), @@ -57,7 +60,7 @@ export function columnIsSortable(column) { const TRANSFORMATION = { numeric: (el) => parseInt(el.innerText), casefold: (el) => el.innerText.toLowerCase().trim(), - tokenData: (el) => el.getAttribute("data-token") + getSortKey: (el) => el.getAttribute("data-sort-by") } /** @@ -68,7 +71,7 @@ export const COLUMN_TRANSFORMATION = { [COLUMN.IDENTIFIER]: TRANSFORMATION.casefold, [COLUMN.NAME]: TRANSFORMATION.casefold, [COLUMN.DESCRIPTION]: TRANSFORMATION.casefold, - [COLUMN.SERVER_ICON]: TRANSFORMATION.tokenData + [COLUMN.SERVER_ICON]: TRANSFORMATION.getSortKey } /** diff --git a/output/main.js b/output/main.js index 3f27bf3c..29fc35b8 100644 --- a/output/main.js +++ b/output/main.js @@ -47,8 +47,7 @@ const transformJoinURL = (join_link) => { } function getTimestamp() { - const timestampRaw = - document.querySelector('meta[name=timestamp]') + const timestampRaw = dom.meta_timestamp() ?.getAttribute('content'); if (!timestampRaw) return null; const timestamp = parseInt(timestampRaw); @@ -62,10 +61,12 @@ function onLoad() { setLastChecked(timestamp); } hideBadCommunities(); - sortTable(COLUMN.NAME); + // Sort by server to show off new feature & align colors. + sortTable(COLUMN.SERVER_ICON); createJoinLinkButtons(); markSortableColumns(); addQRModalHandlers(); + addServerIconInteractions(); } function displayQRModal(communityID) { @@ -77,11 +78,10 @@ function hideQRModal(communityID) { } function addQRModalHandlers() { - const rows = dom.tbl_communities()?.rows; + const rows = dom.tbl_communities_content_rows(); if (!rows) throw new Error("Rows not found"); for (const row of rows) { - if (row.querySelector('th')) continue; - const communityID = row.getAttribute('--data-identifier'); + const communityID = row.getAttribute('data-identifier'); row.querySelector('.td_qr_code').addEventListener( 'click', () => displayQRModal(communityID) @@ -156,6 +156,19 @@ function setLastChecked(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('data-hostname'); + const publicKey = row.getAttribute('data-pubkey'); + const serverIcon = row.querySelector('.td_server_icon'); + serverIcon.addEventListener('click', () => { + alert(`Host: ${hostname}?>\n\nPublic key:\n${publicKey}?>`); + }); + } +} + /** * Function comparing two elements. * diff --git a/output/styles2.css b/output/styles2.css index 786e0146..2aded118 100644 --- a/output/styles2.css +++ b/output/styles2.css @@ -1,5 +1,47 @@ +:root { + --alternate-row-color: #e8e8e8; + --body-margin: 8px; /* Default value in browsers */ + --max-font-size-unitless: 18; + + /* Measurements of the width of all columns + except name and description on smaller screens */ + --collapsed-static-column-width-px: 400px; + --collapsed-static-column-width: calc( + ( 400 / var(--max-font-size-unitless) ) * 1rem + ); + + /* Measurements of the width of all columns + except name and description on wider screens */ + --expanded-static-column-width-px: 800px; + --expanded-static-column-width: calc( + ( 800 / var(--max-font-size-unitless) ) * 1rem + ); + + /* Space left for the name and description columns, + in the collapsed and expanded cases. */ + --collapsed-dynamic-columns-width: + calc( + 100vw + - var(--collapsed-static-column-width) + - 2 * var(--body-margin); + ); + + --expanded-dynamic-columns-width: + calc( + 100vw + - var(--expanded-static-column-width) + ); + + /* Default for wide screens. */ + --dynamic-columns-width: var(--expanded-dynamic-columns-width); +} + html { - font-size: clamp(10px, 2vw, 18px); + font-size: clamp(10px, 2vw, var(--max-font-size-unitless) * 1px); +} + +body { + margin: 0; } html.js .noscript, .hidden { @@ -15,8 +57,8 @@ html.js .noscript, .hidden { text-decoration: underline; } -/* Dead style */ html:not(.js) .js-only { + /* Dead style */ display: none; } @@ -25,6 +67,8 @@ header { direction: row; /* Push items as far apart as possible */ justify-content: space-between; + padding-top: var(--body-margin); + padding-inline: var(--body-margin); } #headline { @@ -32,13 +76,19 @@ header { flex-grow: 1; } +/* --- Table --- */ + #tbl_communities { - /* Browser defaults. */ - --cell-padding-h: 1px; - --cell-padding-v: 1px; - width:100%; + margin: 0 auto; + + --cell-padding-h: 0.5em; + --cell-padding-v: 0.5em; + --cell-padding: var(--cell-padding-h) var(--cell-padding-v); + --cell-padding-small: + calc( var(--cell-padding-h) / 2 ) calc( var(--cell-padding-v) / 2 ); } +/* Cells in general */ #tbl_communities th { white-space: nowrap; } @@ -49,12 +99,12 @@ header { #tbl_communities th.sortable { position: relative; - padding-right: calc( 1.5em + var(--cell-padding-h) ); + padding-right: calc( 1.35em + var(--cell-padding-h) ); } #tbl_communities th.sortable::after { position: absolute; - right: 0.25em; + right: calc( var(--cell-padding-h) ); top: 50%; transform: translateY(-50%); /* content: "\25C7"; */ /* White diamond */ @@ -63,48 +113,143 @@ header { /* content: "\25B8"; */ /* Small right pointing triangle */ content: "\2B25"; /* Black medium diamond */ color: grey; + font-size: 1.3em; } #tbl_communities[data-sort-asc=true] th[data-sort=true]::after { content: "\25B2"; /* Black up pointing triangle */ color: initial; + font-size: 1em; } #tbl_communities[data-sort-asc=false] th[data-sort=true]::after { content: "\25BC"; /* Black up pointing triangle */ color: initial; + font-size: 1em; } -#toggle-show-room-ids:not(:checked) -~ #tbl_communities :is(#th_identifier, .td_identifier) { +#toggle-show-room-ids:not(:checked) +~ #tbl_communities :is(#th_identifier, .td_identifier) { display: none; } +#tbl_communities th { + background-color: lightgray; +} + +#tbl_communities tr:nth-child(even) { + --row-color: white; +} + +#tbl_communities tr:nth-child(odd) { + --row-color: var(--alternate-row-color); + background-color: var(--alternate-row-color); +} + +/* Particular columns */ + .td_identifier { font-family: monospace; } +#th_name, .td_name { + width: calc( + 0.375 * ( + var(--dynamic-columns-width) + ) + ); + max-width: calc( + 0.375 * ( + var(--dynamic-columns-width) + ) + ); + overflow: hidden; + text-overflow: ellipsis; +} + .td_language { text-align: center; - font-size: 1.25em; + font-size: 1.5em; } .td_language:empty::after { content: "\2753"; } -#th_description { } .td_description { + position: relative; overflow: hidden; text-overflow: ellipsis; + /* Prevents middle alignment with table-cell. */ display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; +} + +#th_description, .td_description { + width: calc( + 0.625 * ( + var(--dynamic-columns-width) + ) + ); + + max-width: calc( + 0.625 * ( + var(--dynamic-columns-width) + ) + ); +} + +.td_description::after { + /* Cover overflowing description lines with internal border. */ + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: var(--cell-padding-h); + content: ""; + background-color: var(--row-color); +} + +.td_users { + text-align: center; + /* font-weight: bold; */ +} + +.td_preview { + text-align: center; +} + +.protocol-indicator { + display: inline-block; + font-family: monospace; + border-radius: 4px; + padding: .25em .05em; + width: 6ch; + text-align: center; + text-shadow: 0 0 0.5em #0003; +} + +a[href^="http:"] .protocol-indicator { background-color:lightgray } + +a[href^="https:"] .protocol-indicator { background-color:lightblue } +a[href^="http:"] .protocol-indicator::after { + content: "HTTP"; } -.td_users { text-align: right; } -.td_preview { text-align: center; } -.td_server_icon { text-align: center; } +a[href^="https:"] .protocol-indicator::after { + content: "HTTPS"; +} + +.td_qr_code { + padding: var(--cell-padding-small) !important; +} + +.td_server_icon { + text-align: center; + padding: var(--cell-padding-small) !important; + font-size: 1.1em; +} .td_server_icon-circle { display: flex; @@ -112,11 +257,11 @@ header { justify-content: center; width: 2em; height: 2em; - border-radius: 2em; + border-radius: 100%; font-family: sans-serif; margin: 0 auto; color: white; - text-shadow: 0 0 0.5em #000; + text-shadow: 0 0 0.25em #000a; box-shadow: 0 0 0.05em #777; } @@ -132,18 +277,36 @@ header { } .join_url { + overflow: hidden; + text-overflow: ellipsis; + max-width: 20vw; /* Apply margin against copy button or link. */ /* URL now guaranteed to have interactive element to right when present. */ margin-right: 1em; } -@media (max-width: 950px) { +@media (max-width: 1050px) { /* Only current width breakpoint; */ /* Would follow w4 and precede w6. */ .show-from-w5 { display: none; } + + #th_preview, .td_preview { + display: none; + } + + :root { + --dynamic-columns-width: var(--collapsed-dynamic-columns-width); + } +} + +@media (max-width: 500px) { + :root { + /* ! For when descriptions don't wrap and 100vw doesn't work. */ + --dynamic-columns-width: 15rem; + } } .join_url_container { @@ -153,14 +316,22 @@ header { justify-content: space-around; } -.copy_button { font-size: inherit } +.copy_button { + font-size: 1.1em; + padding: var(--cell-padding); +} + +/* --- Footer --- */ footer { display: flex; flex-direction: column; align-items: center; - width: 100%; + max-width: 100%; text-align: center; + + padding-top: var(--body-margin); + padding-inline: var(--body-margin); } footer p { @@ -184,27 +355,7 @@ label[for=toggle-show-room-ids]::after { content: " (On)" } -/* */ -:root { - --alternate-row-color: #e8e8e8; -} -#tbl_communities th { background-color: lightgray; } -#tbl_communities tr:nth-child(odd) { background-color: var(--alternate-row-color); } - - -.protocol-indicator { - display: inline-block; - font-family: monospace; - border-radius: 4px; - padding: .25em .05em; - width: 6ch; - text-align: center; -} -.protocol-http { background-color:lightgray } -.protocol-https { background-color:lightblue } - - -/* */ +/* --- QR code modals --- */ .qr-code { display: block; margin-left: auto; diff --git a/php/utils/server-utils.php b/php/utils/server-utils.php index 5caa392c..cb632f38 100644 --- a/php/utils/server-utils.php +++ b/php/utils/server-utils.php @@ -118,4 +118,14 @@ return $contents; } } + + function html_sanitize( + ?string $str, int $flags = ENT_QUOTES|ENT_SUBSTITUTE, + ?string $encoding = null, bool $double_encode = true + ) { + if ($str == "") { + return ""; + } + return htmlspecialchars($str, $flags, $encoding, $double_encode); + } ?> diff --git a/sites/+components/tbl_communities.php b/sites/+components/tbl_communities.php index a9dfd40b..ae335ca7 100644 --- a/sites/+components/tbl_communities.php +++ b/sites/+components/tbl_communities.php @@ -10,21 +10,24 @@ function sort_onclick($colno) { global $TABLE_COLUMNS; $column = $TABLE_COLUMNS[$colno]; - if (!column_sortable($column['id'])) return ""; - $name = $column['name']; + $name = isset($column['name_long']) ? $column['name_long'] : $column['name']; + if (!column_sortable($column['id'])) return " title='Column: $name'"; return " title='Click to sort by $name'"; } + // Note: Changing the names displayed requires updating + // the --expanded-static-column-width and --collapsed-static-column-width CSS variables. + $TABLE_COLUMNS = [ - ['id' => "identifier", 'name' => "Identifier"], - ['id' => "language", 'name' => "L"], + ['id' => "identifier", 'name' => "Identifier", 'name_long' => "Room identifier"], + ['id' => "language", 'name' => "L", 'name_long' => "Language"], ['id' => "name", 'name' => "Name"], - ['id' => "description", 'name' => "Description"], - ['id' => "users", 'name' => "Users"], + ['id' => "description", 'name' => "About", 'name_long' => "Description"], + ['id' => "users", 'name' => "#", 'name_long' => "Weekly Active Users"], ['id' => "preview", 'name' => "Preview"], - ['id' => "qr", 'name' => "QR"], - ['id' => "server_icon", 'name' => "Server"], - ['id' => "join_url", 'name' => "Join URL"], + ['id' => "qr_code", 'name' => "QR"], + ['id' => "server_icon", 'name' => "Host", 'name_long' => "Server host"], + ['id' => "join_url", 'name' => "URL", 'name_long' => "In-app Join URL"], ]; ?> @@ -44,43 +47,47 @@ // FIXME: // ! This is bad practice. // However, the fetching code hides component data - // and this is a low risk use case. + // and this is a low risk use case. - $token = explode("=", $room->join_link)[1]; - $icon_hue = hexdec($token[2] . $token[2]); - $icon_color = "hsl($icon_hue, 80%, 50%)"; + $pubkey = explode("=", $room->join_link)[1]; + $icon_hue = hexdec($pubkey[2] . $pubkey[2]); + $icon_color = "hsl($icon_hue, 95%, 50%)"; $hostname = explode("//", $room->join_link)[1]; $hostname = explode("/", $hostname)[0]; // Escape external input. // Ternaries prevent passing null-equal strings, which produce warnings. - $id = htmlspecialchars($id); - $language = $room->language ? htmlspecialchars($room->language) : ""; - $name = htmlspecialchars($room->name); - $desc = $room->description ? htmlspecialchars($room->description) : ""; - $users = htmlspecialchars($room->active_users); - $preview_link = htmlspecialchars($room->preview_link); - $join_link = htmlspecialchars($room->join_link); + $id = html_sanitize($id); + $language = html_sanitize($room->language); + $name = html_sanitize($room->name); + $desc = html_sanitize($room->description); + $users = html_sanitize($room->active_users); + $preview_link = html_sanitize($room->preview_link); + $join_link = html_sanitize($room->join_link); // TODO: Do not forget to rename this escape when merging! - $token = htmlspecialchars($token); - $hostname = htmlspecialchars($hostname); + $token = html_sanitize($token); + $hostname = html_sanitize($hostname); ?> - + - + + + name?> + + + - preview_link, 'http://')): ?> - HTTP - - preview_link, 'https://')): ?> - HTTPS - + @@ -91,12 +98,12 @@ >
- +
diff --git a/sites/+instructions/Czech.txt b/sites/+instructions/Czech.txt index ec5f6e63..4160a067 100644 --- a/sites/+instructions/Czech.txt +++ b/sites/+instructions/Czech.txt @@ -1,6 +1,6 @@ Na mobilu: -- Klikněte na tlačítko Copy v kolonce Join URL. +- Klikněte na tlačítko Copy v kolonce URL. - Otevřete Session, klepněte na tlačítko plus a vyberte "Připojit se ke komunitě". - Klepněte na pole "Zadejte adresu komunity", vložte adresu zkopírovanou v prvním kroku a klepněte na "Připojit se".