1
0
Fork 1

Merge pull request 'Prettify & de-clutter table, UX' (#28) from gravel/sessioncommunities.online:table-improvements into main

Reviewed-on: #28
pull/26/head
SomeGuy 1 year ago
commit 2874d63683

@ -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
}
/**

@ -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.
*

@ -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)"
}
/* <Colors> */
: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 modals --- */
.qr-code {
display: block;
margin-left: auto;

@ -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);
}
?>

@ -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);
?>
<tr id="<?=$id?>" itemscope itemtype="https://schema.org/EntryPoint" --data-identifier="<?=$id?>">
<tr id="<?=$id?>" itemscope itemtype="https://schema.org/EntryPoint"
data-identifier="<?=$id?>"
data-pubkey="<?=$pubkey?>"
data-hostname="<?=$hostname?>"
>
<td class="td_identifier" itemprop="identifier"><?=$id?></td>
<td class="td_language"><?=$language?></td>
<td class="td_name" itemprop="name"><?=$name?></td>
<td class="td_name" title="'<?=$room->name?>' preview" itemprop="name">
<a href="<?=$room->preview_link?>" target="_blank" rel="noopener noreferrer">
<?=$room->name?>
</a>
</td>
<td class="td_description" itemprop="description"><?=$desc?></td>
<td class="td_users"><?=$users?></td>
<td class="td_preview" itemprop="url">
<a href="<?=$preview_link?>" target="_blank" rel="noopener noreferrer nofollow">
<?php if (str_starts_with($room->preview_link, 'http://')): ?>
<span class="protocol-indicator protocol-http">HTTP</span>
<?php endif; ?>
<?php if (str_starts_with($room->preview_link, 'https://')): ?>
<span class="protocol-indicator protocol-https">HTTPS</span>
<?php endif; ?>
<span class="protocol-indicator"></span>
</a>
</td>
<td class="td_qr_code">
@ -91,12 +98,12 @@
>
</td>
<td class="td_server_icon"
data-token="<?=$token?>"
title="<?=$hostname?> (<?=$token?>)"
data-sort-by="<?=$pubkey?>"
title="<?=$hostname?> (<?=$pubkey?>)"
item="image"
>
<div class="td_server_icon-circle" style="background-color: <?=$icon_color?>">
<span><?=strtoupper($token[0] . $token[1])?></span>
<span><?=strtoupper($pubkey[0] . $pubkey[1])?></span>
</div>
</td>
<td class="td_join_url">

@ -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".