Merge branch 'main' into community-labels

dev
gravel 3 years ago
commit 31092d2529
Signed by: gravel
GPG Key ID: C0538F3C906B308F

1
.gitignore vendored

@ -1,6 +1,7 @@
# Generated HTML # Generated HTML
output/*.html output/*.html
# Downloaded QR codes
output/qr-codes output/qr-codes
# Server-side cache # Server-side cache

@ -1,11 +1,12 @@
<?php <?php
$PROJECT_ROOT=__DIR__; $PROJECT_ROOT=__DIR__;
$CACHE_ROOT="$PROJECT_ROOT/cache"; $CACHE_ROOT="$PROJECT_ROOT/cache";
$QR_CODES="$CACHE_ROOT/qr-codes";
$ROOMS_FILE="$CACHE_ROOT/rooms.json"; $ROOMS_FILE="$CACHE_ROOT/rooms.json";
$DOCUMENT_ROOT="$PROJECT_ROOT/output"; $DOCUMENT_ROOT="$PROJECT_ROOT/output";
$TEMPLATES_ROOT="$PROJECT_ROOT/sites"; $TEMPLATES_ROOT="$PROJECT_ROOT/sites";
$LANGUAGES_ROOT="$PROJECT_ROOT/languages"; $LANGUAGES_ROOT="$PROJECT_ROOT/languages";
$QR_CODES="$DOCUMENT_ROOT/qr-codes";
$QR_CODES_RELATIVE="qr-codes";
include_once "$PROJECT_ROOT/php/utils/logging.php"; include_once "$PROJECT_ROOT/php/utils/logging.php";

@ -49,9 +49,10 @@ lan-server:
open: open:
xdg-open "http://localhost:$(PORT)" >/dev/null 2>/dev/null & disown xdg-open "http://localhost:$(PORT)" >/dev/null 2>/dev/null & disown
# Update HTML on file change. Doesn't check for new files. # Update HTML on file change.
watchdog: watchdog:
find . | grep -v ".git" | entr -n -s "$(MAKE) html" set -o pipefail; \
while :; do find . | grep -v ".git" | entr -nds "$(MAKE) html" && exit; done
# Remove artefacts # Remove artefacts
clean: clean:

@ -5,16 +5,14 @@
This script crawls known sources of published Session Communities, This script crawls known sources of published Session Communities,
queries their servers for available information and queries their servers for available information and
displays this information as a static HTML page. displays this information as a static HTML page.
The results of this can be viewed on https://sessioncommunities.online/. The results of this can be viewed on <https://sessioncommunities.online/>.
## What is Session? ## What is Session?
Session is a private messaging app that protects your meta-data, Session is a private messaging app that protects your meta-data,
encrypts your communications, and makes sure your messaging activities encrypts your communications, and makes sure your messaging activities
leave no digital trail behind. leave no digital trail behind.
https://getsession.org/ <https://getsession.org/>
## Details ## Details
@ -59,20 +57,28 @@ The details can be seen in [`curl_get_contents()`](php/utils/utils.php).
### Official repositories ### Official repositories
- GitHub: <https://github.com/mdPlusPlus/sessioncommunities.online> - Codeberg: <https://codeberg.org/gravel/sessioncommunities.online>
- Lokinet Gitea: <https://lokilocker.com/SomeGuy/sessioncommunities.online> - Lokinet Gitea (mirror): <https://lokilocker.com/gravel/sessioncommunities.online>
- GitHub (former official repository): <https://github.com/mdPlusPlus/sessioncommunities.online>
- Lokinet Gitea (former mirror): <https://lokilocker.com/SomeGuy/sessioncommunities.online>
If your favourite Session community is missing a language flag, If your favourite Session community is missing a language flag,
you can issue a pull request here: you can issue a pull request here:
- <https://github.com/mdPlusPlus/sessioncommunities.online-languages/> - <https://github.com/mdPlusPlus/sessioncommunities.online-languages/>
Alternatively, support this issue on Oxen Github to make language tags a native feature:
- <https://github.com/oxen-io/session-pysogs/issues/186>
### Contributing ### Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md). See [CONTRIBUTING.md](CONTRIBUTING.md).
## Contact ## Contact
If you want to contact me, you can add me on Session via my To report issues, visit the Web Development Community on [caliban.org](http://sog.caliban.org/r/webdev).
[ONS](https://docs.oxen.io/using-the-oxen-blockchain/using-oxen-name-system):
`someguy`. Contact gravel on Session as `gravel` (Registered ID) or [read more](https://codeberg.org/gravel/gravel).
Contact SomeGuy on Session as `someguy` (Registered ID).

@ -1,7 +1,21 @@
body {
background-color: black;
color: white;
}
header { header {
text-align: center; text-align: center;
} }
a, .anchorstyle {
color: hsl(210, 100%, 60%);
text-decoration: underline;
}
#language-selection-title, label[for^=language-selection] {
font-size: 1.2em;
}
#instructions { #instructions {
padding: 1em; padding: 1em;
} }
@ -14,7 +28,7 @@ header {
font-size: 1.25em; font-size: 1.25em;
line-height: 1.5; line-height: 1.5;
padding: 2em; padding: 2em;
background-color: silver; background-color: hsl(0, 0%, 9%);
border: 2px solid black; border: 2px solid black;
border-radius: 1em; border-radius: 1em;
} }

@ -1,5 +1,4 @@
:root { :root {
--alternate-row-color: #e8e8e8;
--body-margin: 8px; /* Default value in browsers */ --body-margin: 8px; /* Default value in browsers */
--max-font-size-unitless: 18; --max-font-size-unitless: 18;
@ -36,6 +35,14 @@
--dynamic-columns-width: var(--expanded-dynamic-columns-width); --dynamic-columns-width: var(--expanded-dynamic-columns-width);
} }
:root {
--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 );
}
html { html {
font-size: clamp(10px, 2vw, var(--max-font-size-unitless) * 1px); font-size: clamp(10px, 2vw, var(--max-font-size-unitless) * 1px);
} }
@ -44,17 +51,51 @@ body {
margin: 0; margin: 0;
} }
html.js .noscript, .hidden { #toggle-theme-switch {
display: none; display: none;
} }
.clickable { #theming-root {
cursor: pointer; width: 100%;
height: 100%;
margin: 0;
background-color: var(--secondary-color);
color: var(--primary-color);
}
#toggle-theme-switch:not(:checked) ~ #theming-root {
--primary-color: hsl(0, 0%, 100%);
--secondary-color: hsl(0, 0%, 0%);
--secondary-color-heading: hsl(0, 0%, 17%);
--secondary-color-shaded: hsl(0, 0%, 9%);
--anchor-color: hsl(210, 100%, 60%);
--color-http: hsl(0, 0%, 17%);
--color-https: hsl(195, 53%, 21%);
}
#toggle-theme-switch:checked ~ #theming-root {
--primary-color: hsl(0, 0%, 0%);
--secondary-color: hsl(0, 0%, 100%);
--secondary-color-heading: hsl(0, 0%, 83%);
--secondary-color-shaded: hsl(0, 0%, 91%);
--anchor-color: hsl(210, 100%, 40%);
--color-http: hsl(0, 0%, 83%);
--color-https: hsl(195, 53%, 79%)
} }
.anchorstyle { a, .anchorstyle {
color: rgb(0, 102, 204); color: var(--anchor-color);
text-decoration: underline; text-decoration: underline;
cursor: pointer;
}
*, *::after, *::before {
transition: color 3s, background-color 2s;
}
html.js .noscript, .hidden {
display: none;
} }
html:not(.js) .js-only { html:not(.js) .js-only {
@ -84,6 +125,10 @@ html:not(.js) .js-only {
background-color: pink; background-color: pink;
} }
gap {
flex-grow: 1000;
}
header { header {
display: flex; display: flex;
direction: row; direction: row;
@ -93,6 +138,11 @@ header {
padding-inline: var(--body-margin); padding-inline: var(--body-margin);
} }
#header-start, #header-end {
display: flex;
gap: 0.5em;
}
#headline { #headline {
text-align: center; text-align: center;
flex-grow: 1; flex-grow: 1;
@ -102,12 +152,6 @@ header {
#tbl_communities { #tbl_communities {
margin: 0 auto; 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 */ /* Cells in general */
@ -156,16 +200,21 @@ header {
} }
#tbl_communities th { #tbl_communities th {
background-color: lightgray; background-color: var(--secondary-color-heading);
}
#tbl_communities tr {
background-color: var(--row-color);
} }
#tbl_communities tr:nth-child(even) { #tbl_communities tr:nth-child(even) {
--row-color: white; background-color: var(--secondary-color);
--row-color: var(--secondary-color);
} }
#tbl_communities tr:nth-child(odd) { #tbl_communities tr:nth-child(odd) {
--row-color: var(--alternate-row-color); background-color: var(--secondary-color-shaded);
background-color: var(--alternate-row-color); --row-color: var(--secondary-color-shaded);
} }
/* Particular columns */ /* Particular columns */
@ -251,9 +300,13 @@ header {
text-shadow: 0 0 0.5em #0003; text-shadow: 0 0 0.5em #0003;
} }
a[href^="http:"] .protocol-indicator { background-color:lightgray } a[href^="http:"] .protocol-indicator {
background-color: var(--color-http);
}
a[href^="https:"] .protocol-indicator { background-color:lightblue } a[href^="https:"] .protocol-indicator {
background-color: var(--color-https);
}
a[href^="http:"] .protocol-indicator::after { a[href^="http:"] .protocol-indicator::after {
content: "HTTP"; content: "HTTP";
@ -316,7 +369,10 @@ a[href^="https:"] .protocol-indicator::after {
.copy_button { .copy_button {
font-size: 1.1em; font-size: 1.1em;
background-color: var(--secondary-color);
color: var(--primary-color);
padding: var(--cell-padding); padding: var(--cell-padding);
border-radius: 10%;
} }
/* --- Footer --- */ /* --- Footer --- */
@ -354,47 +410,70 @@ label[for=toggle-show-room-ids]::after {
} }
/* --- QR code modals --- */ /* --- QR code modals --- */
.qr-code { #details-modal {
display: block; padding: 0;
margin-left: auto; width: 80vw;
margin-right: auto; max-height: 80vh;
width: 50%; color: var(--primary-color);
background-color: var(--secondary-color);
} }
.qr-code-icon {
cursor: pointer; #details-modal-contents {
display: flex;
position: relative;
flex-direction: row;
padding: 3em;
} }
.qr-code-modal {
display: none; /* Hidden by default */ #details-modal-close {
position: fixed; /* Stay in place */ position: absolute;
z-index: 1; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
padding-top: 100px; /* Location of the box */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
/*overflow: auto;*/ /* Enable scroll if needed */
}
.qr-code-modal-content {
background-color: #ffffff;
border: 1px solid #000000;
width: 80%;
margin: auto;
padding: 20px;
}
.qr-code-modal-close {
float: right;
font-size: 35px;
font-weight: bold;
color: #aaaaaa;
}
.qr-code-modal-close:hover,
.qr-code-modal-close:focus {
cursor: pointer; cursor: pointer;
text-decoration: none; top: 0rem;
color: #000000; right: 0rem;
font-size: 3.5rem;
width: 5rem;
height: 5.5rem;
text-align: center;
}
#details-modal-start {
display: flex;
flex-direction: column;
margin-right: 1em;
}
#details-modal-end {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#details-modal-start #details-modal-description {
max-height: 50vh;
overflow: auto;
}
#details-modal-start #details-modal-description-inner:empty::after {
content: "No description";
font-style: italic;
}
#details-modal-end #details-modal-qr-code {
width: 20em;
height: 20em;
margin-bottom: 1em;
}
#details-modal-end #details-modal-qr-code-label {
text-align: center;
}
#details-modal :is(#details-modal-copy-button, #details-modal-copy-staff-id) {
color: var(--primary-color);
background-color: var(--secondary-color);
border-radius: 10%;
padding: var(--cell-padding-small);
} }
/* <Snackbar> */ /* <Snackbar> */
@ -423,7 +502,6 @@ label[for=toggle-show-room-ids]::after {
} }
/* Responsive properties */ /* Responsive properties */
.td_name .room-label:nth-of-type(5) ~ .room-label { .td_name .room-label:nth-of-type(5) ~ .room-label {
display: none; display: none;
} }
@ -470,10 +548,6 @@ label[for=toggle-show-room-ids]::after {
:root { :root {
--dynamic-columns-width: var(--collapsed-dynamic-columns-width); --dynamic-columns-width: var(--collapsed-dynamic-columns-width);
} }
.td_name .room-label:nth-of-type(1) ~ .room-label {
display: none;
}
} }
@media (max-width: 500px) { @media (max-width: 500px) {
@ -481,6 +555,10 @@ label[for=toggle-show-room-ids]::after {
/* ! For when descriptions don't wrap and 100vw doesn't work. */ /* ! For when descriptions don't wrap and 100vw doesn't work. */
--dynamic-columns-width: 15rem; --dynamic-columns-width: 15rem;
} }
#details-modal-contents {
flex-direction: column;
}
} }
/* Animations to fade the snackbar in and out */ /* Animations to fade the snackbar in and out */

@ -7,14 +7,36 @@ export const dom = {
tbl_communities: () => document.getElementById("tbl_communities"), tbl_communities: () => document.getElementById("tbl_communities"),
tbl_communities_content_rows: tbl_communities_content_rows:
() => Array.from(dom.tbl_communities()?.rows)?.filter(row => !row.querySelector('th')), () => Array.from(dom.tbl_communities()?.rows)?.filter(row => !row.querySelector('th')),
community_row: (communityID) => document.getElementById(communityID),
row_info: (row) => {
/** @type {string[]} */
return {
language_flag: row.querySelector('.td_language').textContent.trim(),
name: row.querySelector('.td_name').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: row.querySelector('.td_join_url a[href]').getAttribute('href'),
hostname: row.getAttribute('data-hostname'),
public_key: row.getAttribute('data-pubkey'),
staff: row.getAttribute('data-staff')
};
},
meta_timestamp: () => document.querySelector('meta[name=timestamp]'), meta_timestamp: () => document.querySelector('meta[name=timestamp]'),
last_checked: () => document.getElementById("last_checked_value"), last_checked: () => document.getElementById("last_checked_value"),
qr_modal: (communityID) => document.getElementById(`modal_${communityID}`), /** @return {HTMLDialogElement | null} */
details_modal: () => document.getElementById('details-modal'),
details_modal_qr_code: () => document.getElementById('details-modal-qr-code'),
join_urls: () => document.getElementsByClassName("join_url_container"), join_urls: () => document.getElementsByClassName("join_url_container"),
servers_hidden: () => document.getElementById("servers_hidden"), servers_hidden: () => document.getElementById("servers_hidden"),
snackbar: () => document.getElementById("copy-snackbar") snackbar: () => document.getElementById("copy-snackbar"),
qr_code_buttons: () => document.querySelectorAll('.qr-code-button'),
} }
export const JOIN_URL_PASTE = "Copied URL to clipboard. Paste into Session app to join";
export const communityQRCodeURL = (communityID) => `qr-codes/${communityID}.png`
export const COLUMN = { export const COLUMN = {
IDENTIFIER: 0, LANGUAGE: 1, NAME: 2, IDENTIFIER: 0, LANGUAGE: 1, NAME: 2,
DESCRIPTION: 3, USERS: 4, PREVIEW: 5, DESCRIPTION: 3, USERS: 4, PREVIEW: 5,
@ -33,11 +55,20 @@ export const COMPARISON = {
}; };
export const ATTRIBUTES = { export const ATTRIBUTES = {
ROW: {
IDENTIFIER: 'data-identifier',
PUBLIC_KEY: 'data-pubkey',
HOSTNAME: 'data-hostname',
STAFF_DATA: 'data-staff'
},
SORTING: { SORTING: {
ACTIVE: 'data-sort', ACTIVE: 'data-sort',
ASCENDING: 'data-sort-asc', ASCENDING: 'data-sort-asc',
COLUMN: 'data-sorted-by', COLUMN: 'data-sorted-by',
// COLUMN_LITERAL: 'sorted-by' // COLUMN_LITERAL: 'sorted-by'
},
HYDRATION: {
CONTENT: 'data-hydrate-with'
} }
}; };

@ -1,6 +1,6 @@
// Hello reader! // Hello reader!
// This project can be found at: // This project can be found at:
// https://lokilocker.com/someguy/sessioncommunities.online // https://codeberg.com/gravel/sessioncommunities.online
/** /**
* This JavaScript file uses the JSDoc commenting style. * This JavaScript file uses the JSDoc commenting style.
@ -16,7 +16,8 @@
// Import magic numbers and data // Import magic numbers and data
import { import {
dom, COLUMN, COLUMN_LITERAL, COMPARISON, ATTRIBUTES, dom, COLUMN, COLUMN_LITERAL, COMPARISON, ATTRIBUTES,
columnAscendingByDefault, columnIsSortable, COLUMN_TRANSFORMATION, element columnAscendingByDefault, columnIsSortable, COLUMN_TRANSFORMATION,
element, JOIN_URL_PASTE, communityQRCodeURL
} from './js/constants.js'; } from './js/constants.js';
// Hidden communities for transparency. // Hidden communities for transparency.
@ -45,6 +46,7 @@ const transformJoinURL = (join_link) => {
return element.button({ return element.button({
textContent: "Copy", textContent: "Copy",
className: "copy_button", className: "copy_button",
title: "Click here to copy the join URL",
onclick: () => copyToClipboard(join_link) onclick: () => copyToClipboard(join_link)
}); });
} }
@ -70,31 +72,122 @@ function onLoad() {
markSortableColumns(); markSortableColumns();
addQRModalHandlers(); addQRModalHandlers();
addServerIconInteractions(); addServerIconInteractions();
preloadQRCodes();
} }
function displayQRModal(communityID) { function displayQRModal(communityID) {
dom.qr_modal(communityID).style.display = "block"; const modal = dom.details_modal();
if (!modal) {
throw new DOMException("Modal element not found.");
}
const row = dom.community_row(communityID);
if (!row) {
throw new DOMException("Community row not found.");
}
const rowInfo = dom.row_info(row);
for (const element of modal.querySelectorAll(`[${ATTRIBUTES.HYDRATION.CONTENT}]`)) {
const attributes = element.getAttribute(ATTRIBUTES.HYDRATION.CONTENT);
if (!attributes) continue;
for (const attribute of attributes.split(';')) {
const [property, targetProperty] = attribute.includes(':')
? attribute.split(":")
: [attribute, 'textContent'];
if (!Object.getOwnPropertyNames(rowInfo).includes(property)) {
console.error(`Unknown rowInfo property: ${property}`);
continue;
}
if (targetProperty === 'textContent') {
element.textContent = rowInfo[property];
} else {
element.setAttribute(targetProperty, rowInfo[property]);
}
}
}
dom.details_modal_qr_code().src = communityQRCodeURL(communityID);
modal.showModal();
} }
function hideQRModal(communityID) { function hideQRModal(communityID) {
dom.qr_modal(communityID).style.display = "none"; dom.details_modal().close();
} }
function addQRModalHandlers() { function addQRModalHandlers() {
const rows = dom.tbl_communities_content_rows(); const rows = dom.tbl_communities_content_rows();
if (!rows) throw new Error("Rows not found"); if (!rows) throw new Error("Rows not found");
for (const row of rows) { for (const row of rows) {
const communityID = row.getAttribute('data-identifier'); const communityID = row.getAttribute(ATTRIBUTES.ROW.IDENTIFIER);
row.querySelector('.td_qr_code').addEventListener( row.querySelector('.td_qr_code').addEventListener(
'click', 'click',
() => displayQRModal(communityID) () => displayQRModal(communityID)
); );
row.querySelector('.td_name').addEventListener(
'click',
(e) => {
e.preventDefault();
displayQRModal(communityID);
}
);
}
const closeButton = const closeButton =
dom.qr_modal(communityID).querySelector('.qr-code-modal-close'); dom.details_modal().querySelector('#details-modal-close');
closeButton.addEventListener( closeButton.addEventListener(
'click', 'click',
() => hideQRModal(communityID) () => hideQRModal()
);
dom.details_modal().addEventListener('click', function (e) {
if (this == e.target) {
this.close();
}
});
document.querySelector('#details-modal-copy-button').addEventListener(
'click',
function () {
copyToClipboard(this.getAttribute('data-href'));
}
)
document.querySelector('#details-modal-copy-staff-id')?.addEventListener(
'click',
function () {
/**
* @type {string[]}
*/
const staff = JSON.parse(this.getAttribute(ATTRIBUTES.ROW.STAFF_DATA));
if (staff.length == 0) {
alert("No public moderators available for this Community.");
return;
}
const staffId = staff[~~(staff.length * Math.random())];
copyToClipboard(`@${staffId}`, 'Copied staff ID to clipboard.');
}
)
for (const anchor of dom.qr_code_buttons()) {
// Disable QR code links
anchor.setAttribute("href", "#");
anchor.removeAttribute("target");
anchor.addEventListener('click', (e) => { e.preventDefault(); return false });
}
}
function preloadQRCodes() {
const rows = dom.tbl_communities_content_rows();
const identifiers = rows.map(
rowElement => rowElement.getAttribute(ATTRIBUTES.ROW.IDENTIFIER)
); );
for (const identifier of identifiers) {
(new Image()).src = communityQRCodeURL(identifier);
} }
} }
@ -133,13 +226,20 @@ function hideElementByID(id) {
/** /**
* Copies text to clipboard and shows an informative toast. * Copies text to clipboard and shows an informative toast.
* @param {string} text - Text to copy to clipboard. * @param {string} text - Text to copy to clipboard.
* @param {string} [toastText] - Text shown by toast.
*/ */
function copyToClipboard(text) { function copyToClipboard(text, toastText = JOIN_URL_PASTE) {
navigator.clipboard.writeText(text); navigator.clipboard.writeText(text);
// Find snackbar element // Find snackbar element
const snackbar = dom.snackbar(); const snackbar = dom.snackbar();
if (!snackbar) {
throw new DOMException("Could not find snackbar");
}
snackbar.textContent = toastText;
snackbar.classList.add('show') snackbar.classList.add('show')
// After 3 seconds, hide the snackbar. // After 3 seconds, hide the snackbar.
@ -163,8 +263,8 @@ function setLastChecked(last_checked) {
function addServerIconInteractions() { function addServerIconInteractions() {
const rows = dom.tbl_communities_content_rows(); const rows = dom.tbl_communities_content_rows();
for (const row of rows) { for (const row of rows) {
const hostname = row.getAttribute('data-hostname'); const hostname = row.getAttribute(ATTRIBUTES.ROW.HOSTNAME);
const publicKey = row.getAttribute('data-pubkey'); const publicKey = row.getAttribute(ATTRIBUTES.ROW.PUBLIC_KEY);
const serverIcon = row.querySelector('.td_server_icon'); const serverIcon = row.querySelector('.td_server_icon');
serverIcon.addEventListener('click', () => { serverIcon.addEventListener('click', () => {
alert(`Host: ${hostname}\n\nPublic key:\n${publicKey}`); alert(`Host: ${hostname}\n\nPublic key:\n${publicKey}`);

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M48 32C21.5 32 0 53.5 0 80v96c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48H48zm80 64v64H64V96h64zM48 288c-26.5 0-48 21.5-48 48v96c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48V336c0-26.5-21.5-48-48-48H48zm80 64v64H64V352h64zM256 80v96c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48H304c-26.5 0-48 21.5-48 48zm64 16h64v64H320V96zm32 352v32h32V448H352zm96 0H416v32h32V448zM416 288v32H352V288H256v96 96h64V384h32v32h96V352 320 288H416z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path style="fill: grey;" d="M48 32C21.5 32 0 53.5 0 80v96c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48H48zm80 64v64H64V96h64zM48 288c-26.5 0-48 21.5-48 48v96c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48V336c0-26.5-21.5-48-48-48H48zm80 64v64H64V352h64zM256 80v96c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48H304c-26.5 0-48 21.5-48 48zm64 16h64v64H320V96zm32 352v32h32V448H352zm96 0H416v32h32V448zM416 288v32H352V288H256v96 96h64V384h32v32h96V352 320 288H416z"/></svg>

Before

Width:  |  Height:  |  Size: 725 B

After

Width:  |  Height:  |  Size: 745 B

@ -0,0 +1,39 @@
<?php
/**
* Return local path to room invite code.
* @param string $room_id Id of room to locate QR code for.
*/
function room_qr_code_path(string $room_id): string {
global $QR_CODES;
return "$QR_CODES/$room_id.png";
}
/**
* Return remote path to room invite code.
* @param string $room_id Id of room to locate QR code for.
*/
function room_qr_code_path_relative(string $room_id): string {
global $QR_CODES_RELATIVE;
return "$QR_CODES_RELATIVE/$room_id.png";
}
/**
* Fetch QR invite of the given room and return its local path.
* @param \CommunityRoom $room
* @return string
*/
function room_qr_code($room): string {
$room_id = $room->get_room_identifier();
$png_cached = room_qr_code_path($room_id);
if (file_exists($png_cached)) {
return room_qr_code_path_relative($room_id);
}
log_debug("Fetching QR code for $room_id.");
$png = file_get_contents($room->get_invite_url());
file_put_contents($png_cached, $png);
return room_qr_code_path_relative($room_id);
}
file_exists($QR_CODES) or mkdir($QR_CODES, 0700);
?>

@ -1,46 +1,74 @@
<?php <?php
/** require_once "$PROJECT_ROOT/php/utils/room-invites.php";
* @var CommunityRoom[] $rooms
*/
function room_qr_code_cached($room_id) {
global $QR_CODES;
return "$QR_CODES/$room_id.png";
}
/**
* Fetch QR codes from SOGS server and encode them as base64
* @param CommunityRoom $room
*/
function base64_qr_code($room, $size = "512x512") {
$room_id = $room->get_room_identifier();
$png_cached = room_qr_code_cached($room_id);
if (file_exists($png_cached)) {
return base64_encode(file_get_contents($png_cached));
}
log_debug("Fetching QR code for $room_id.");
$png = file_get_contents($room->get_invite_url());
file_put_contents($png_cached, $png);
return base64_encode($png);
}
file_exists($QR_CODES) or mkdir($QR_CODES, 0700);
?> ?>
<div id="modal-container"> <dialog id="details-modal">
<?php foreach ($rooms as $room): ?> <div id="details-modal-contents">
<div id="modal_<?=$room->get_room_identifier()?>" class="qr-code-modal"> <div id="details-modal-close">
<div class="qr-code-modal-content">
<span class="qr-code-modal-close">
&times; &times;
</span> </div>
<div id="details-modal-start">
<h1 id="details-modal-title">
<a
id="details-modal-community-name"
data-hydrate-with="name;preview_link:href"
title="Open preview in new tab"
></a>
</h1>
<p id="details-modal-description">
<span>Description:</span>
<span id="details-modal-description-inner" data-hydrate-with="description"></span>
</p>
<gap></gap>
<div id="details-modal-room-info">
<p>
Language: <span data-hydrate-with="language_flag"></span>
</p>
<p>
Users: <span data-hydrate-with="users"></span>
</p>
<p>
Server:
<a
title="Open server in new tab"
data-hydrate-with="hostname;hostname:href"
target="_blank"
rel="noopener noreferrer"
></a>
</p>
<p>
<button
id="details-modal-copy-button"
data-hydrate-with="join_link:data-href"
title="Click here to copy this Community's join link"
>
Copy join link
</button>
<button
id="details-modal-copy-staff-id"
data-hydrate-with="staff:data-staff"
title="Copy the mention for a random staff member"
>
Copy mod ID
</button>
</p>
</div>
</div>
<gap></gap>
<div id="details-modal-end">
<img <img
src="data:image/png;base64,<?=base64_qr_code($room)?>" src=""
alt="Community join link encoded as QR code" id="details-modal-qr-code"
class="qr-code" title="Community join link encoded as QR code"
loading="lazy"
> >
<div id="details-modal-qr-code-label">
Scan QR code in Session to join
<br>
'<span data-hydrate-with="name"></span>'
</div>
</div> </div>
</div> </div>
<?php endforeach; ?> </dialog>
</div>

@ -1,6 +1,7 @@
<?php <?php
require_once "$PROJECT_ROOT/php/utils/utils.php"; require_once "$PROJECT_ROOT/php/utils/utils.php";
require_once "$PROJECT_ROOT/php/utils/servers-rooms.php"; require_once "$PROJECT_ROOT/php/utils/servers-rooms.php";
require_once "$PROJECT_ROOT/php/utils/room-invites.php";
/** /**
* @var CommunityRoom[] $rooms * @var CommunityRoom[] $rooms
@ -17,7 +18,7 @@
$column = $TABLE_COLUMNS[$colno]; $column = $TABLE_COLUMNS[$colno];
$name = isset($column['name_long']) ? $column['name_long'] : $column['name']; $name = isset($column['name_long']) ? $column['name_long'] : $column['name'];
if (!column_sortable($column['id'])) return " title='$name'"; if (!column_sortable($column['id'])) return " title='$name'";
return " title='Click to sort by $name'."; return " title='Click to sort by $name.'";
} }
// Note: Changing the names displayed requires updating // Note: Changing the names displayed requires updating
@ -63,12 +64,15 @@
$join_link = html_sanitize($room->get_join_url()); $join_link = html_sanitize($room->get_join_url());
$pubkey = html_sanitize($pubkey); $pubkey = html_sanitize($pubkey);
$hostname = html_sanitize($hostname); $hostname = html_sanitize($hostname);
$staff_json = json_encode(array_map('html_sanitize', $room->get_staff()));
?> ?>
<tr id="<?=$id?>" itemscope itemtype="https://schema.org/EntryPoint" <tr id="<?=$id?>" class="room-row" itemscope itemtype="https://schema.org/EntryPoint"
data-identifier="<?=$id?>" data-identifier="<?=$id?>"
data-pubkey="<?=$pubkey?>" data-pubkey="<?=$pubkey?>"
data-hostname="<?=$hostname?>" data-hostname="<?=$hostname?>"
data-staff='<?=$staff_json?>'
> >
<td class="td_identifier" itemprop="identifier"><?=$id?></td> <td class="td_identifier" itemprop="identifier"><?=$id?></td>
<td class="td_language" title="Language flag for '<?=$name?>'"><?=$language?></td> <td class="td_language" title="Language flag for '<?=$name?>'"><?=$language?></td>
@ -117,12 +121,18 @@
</a> </a>
</td> </td>
<td class="td_qr_code"> <td class="td_qr_code">
<a
class="qr-code-button"
href="<?=room_qr_code($room)?>"
target="_blank"
>
<img <img
class="qr-code-icon" class="qr-code-icon"
src="qrcode-solid.svg" src="qrcode-solid.svg"
alt="Pictogram of a QR code" alt="Pictogram of a QR code"
title="Click here to view the QR Code for '<?=$name?>'" title="Click here to view the details for '<?=$name?>'"
> >
</a>
</td> </td>
<td class="td_server_icon" <td class="td_server_icon"
data-sort-by="<?=$pubkey?>" data-sort-by="<?=$pubkey?>"
@ -135,8 +145,8 @@
</td> </td>
<td class="td_join_url"> <td class="td_join_url">
<div class="join_url_container" data-url="<?=$join_link?>"> <div class="join_url_container" data-url="<?=$join_link?>">
<a class="join_url show-from-w5" title="<?=$join_link?>" <span class="join_url show-from-w5" title="<?=$join_link?>"
><?=truncate($join_link, 32)?></a> ><?=truncate($join_link, 32)?></span>
<a <a
class="noscript" class="noscript"
title="Copy this link to join '<?=$name?>'." title="Copy this link to join '<?=$name?>'."

@ -31,7 +31,7 @@
<?php include "+components/page-head.php" ?> <?php include "+components/page-head.php" ?>
<link rel="canonical" href="https://sessioncommunities.online/"> <link rel="canonical" href="https://sessioncommunities.online/">
<link rel="stylesheet" href="styles2.css"> <link rel="stylesheet" href="./index.css">
<script type="module" src="./main.js"></script> <script type="module" src="./main.js"></script>
<title>Self-updating list of active Session communities</title> <title>Self-updating list of active Session communities</title>
<meta name="description" content=" <meta name="description" content="
@ -51,9 +51,18 @@
<meta name="timestamp" content="<?=$timestamp?>"> <meta name="timestamp" content="<?=$timestamp?>">
</head> </head>
<body> <body>
<input type="checkbox" id="toggle-theme-switch">
<div id="theming-root">
<header> <header>
<div id="header-start"></div> <div id="header-start"></div>
<div id="header-end"> <div id="header-end">
<label
for="toggle-theme-switch"
class="anchorstyle"
title="Switch color theme"
>
Switch theme
</label>
<a <a
id="link-instructions" id="link-instructions"
target="_blank" target="_blank"
@ -92,7 +101,7 @@
<br> <br>
Communities shown are fetched automatically from Communities shown are fetched automatically from
<a <a
href="https://github.com/mdPlusPlus/sessioncommunities.online#which-sources-are-crawled" href="https://codeberg.org/gravel/sessioncommunities.online#which-sources-are-crawled"
target="_blank" target="_blank"
>various sources</a>. >various sources</a>.
<br> <br>
@ -144,25 +153,19 @@
</nav> </nav>
<nav> <nav>
<a <a
href="https://github.com/mdPlusPlus/sessioncommunities.online" href="https://codeberg.org/gravel/sessioncommunities.online"
target="_blank" target="_blank"
title="sessioncommunities.online repository on GitHub." title="sessioncommunities.online repository on Codeberg."
>Source Code</a> >Source Code</a>
<a <a
href="https://lokilocker.com/someguy/sessioncommunities.online" href="https://codeberg.org/gravel/sessioncommunities.online#contact"
target="_blank"
title="sessioncommunities.online repository on Lokinet Gitea."
>Source Code (Mirror)</a>
<a
href="https://github.com/mdPlusPlus/sessioncommunities.online#contact"
target="_blank" target="_blank"
rel="author" rel="author"
title="Information on how to contact the maintainer of sessioncommunities.online" title="Information on how to contact the maintainer of sessioncommunities.online"
>Contact</a> >Contact</a>
</nav> </nav>
</footer> </footer>
<div id="copy-snackbar">
Copied URL to clipboard. Paste into Session app to join
</div> </div>
<div id="copy-snackbar"></div>
</body> </body>
</html> </html>

@ -41,9 +41,11 @@
Go back to Community list Go back to Community list
</a> </a>
</p> </p>
<span id="language-selection-title">
Choose your language: Choose your language:
<?php foreach ($instruction_files as $i => $file): ?> </span>
<br> <br>
<?php foreach ($instruction_files as $i => $file): ?>
<input <input
id="language-selection-<?=$i?>" id="language-selection-<?=$i?>"
class="language-selection" class="language-selection"

Loading…
Cancel
Save