diff --git a/.gitignore b/.gitignore index 08d9146..4e4e3c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ # Generated HTML output/*.html +# Downloaded QR codes + +output/qr-codes + # Server-side cache cache diff --git a/.phpenv b/.phpenv index a57fb7f..cee30a4 100644 --- a/.phpenv +++ b/.phpenv @@ -1,11 +1,12 @@ */ @@ -475,6 +478,34 @@ label[for=toggle-show-room-ids]::after { animation: fadein 0.5s, fadeout 0.5s 2.5s; } + +@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; + } + + #details-modal-contents { + flex-direction: column; + } +} + /* Animations to fade the snackbar in and out */ @-webkit-keyframes fadein { from {bottom: 0; opacity: 0;} diff --git a/output/js/constants.js b/output/js/constants.js index 9985128..ea946c5 100644 --- a/output/js/constants.js +++ b/output/js/constants.js @@ -7,14 +7,36 @@ export const dom = { tbl_communities: () => document.getElementById("tbl_communities"), tbl_communities_content_rows: () => 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]'), 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"), 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 = { IDENTIFIER: 0, LANGUAGE: 1, NAME: 2, DESCRIPTION: 3, USERS: 4, PREVIEW: 5, @@ -33,11 +55,20 @@ export const COMPARISON = { }; export const ATTRIBUTES = { + ROW: { + IDENTIFIER: 'data-identifier', + PUBLIC_KEY: 'data-pubkey', + HOSTNAME: 'data-hostname', + STAFF_DATA: 'data-staff' + }, SORTING: { ACTIVE: 'data-sort', ASCENDING: 'data-sort-asc', COLUMN: 'data-sorted-by', // COLUMN_LITERAL: 'sorted-by' + }, + HYDRATION: { + CONTENT: 'data-hydrate-with' } }; diff --git a/output/main.js b/output/main.js index 3252845..f8412f1 100644 --- a/output/main.js +++ b/output/main.js @@ -16,7 +16,8 @@ // Import magic numbers and data import { dom, COLUMN, COLUMN_LITERAL, COMPARISON, ATTRIBUTES, - columnAscendingByDefault, columnIsSortable, COLUMN_TRANSFORMATION, element + columnAscendingByDefault, columnIsSortable, COLUMN_TRANSFORMATION, + element, JOIN_URL_PASTE, communityQRCodeURL } from './js/constants.js'; // Hidden communities for transparency. @@ -71,31 +72,122 @@ function onLoad() { markSortableColumns(); addQRModalHandlers(); addServerIconInteractions(); + preloadQRCodes(); } 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) { - dom.qr_modal(communityID).style.display = "none"; + dom.details_modal().close(); } function addQRModalHandlers() { const rows = dom.tbl_communities_content_rows(); if (!rows) throw new Error("Rows not found"); for (const row of rows) { - const communityID = row.getAttribute('data-identifier'); + const communityID = row.getAttribute(ATTRIBUTES.ROW.IDENTIFIER); row.querySelector('.td_qr_code').addEventListener( 'click', () => displayQRModal(communityID) ); - const closeButton = - dom.qr_modal(communityID).querySelector('.qr-code-modal-close'); - closeButton.addEventListener( + row.querySelector('.td_name').addEventListener( 'click', - () => hideQRModal(communityID) + (e) => { + e.preventDefault(); + displayQRModal(communityID); + } ); + + } + const closeButton = + dom.details_modal().querySelector('#details-modal-close'); + closeButton.addEventListener( + 'click', + () => 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); } } @@ -134,13 +226,20 @@ function hideElementByID(id) { /** * Copies text to clipboard and shows an informative toast. * @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); // Find snackbar element const snackbar = dom.snackbar(); + if (!snackbar) { + throw new DOMException("Could not find snackbar"); + } + + snackbar.textContent = toastText; + snackbar.classList.add('show') // After 3 seconds, hide the snackbar. @@ -164,8 +263,8 @@ function setLastChecked(last_checked) { 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 hostname = row.getAttribute(ATTRIBUTES.ROW.HOSTNAME); + const publicKey = row.getAttribute(ATTRIBUTES.ROW.PUBLIC_KEY); const serverIcon = row.querySelector('.td_server_icon'); serverIcon.addEventListener('click', () => { alert(`Host: ${hostname}\n\nPublic key:\n${publicKey}`); diff --git a/php/utils/room-invites.php b/php/utils/room-invites.php new file mode 100644 index 0000000..8ec5bb5 --- /dev/null +++ b/php/utils/room-invites.php @@ -0,0 +1,39 @@ +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); +?> \ No newline at end of file diff --git a/sites/+components/qr_modals.php b/sites/+components/qr_modals.php index 8d66b06..bd7c67a 100644 --- a/sites/+components/qr_modals.php +++ b/sites/+components/qr_modals.php @@ -1,46 +1,74 @@ - /** - * 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); - } + +
+
+ × +
+
+

+ +

+

+ Description: + +

+ +
+

+ Language: +

+

+ Users: +

+

+ Server: + +

- file_exists($QR_CODES) or mkdir($QR_CODES, 0700); -?> +

+ -

- -
+
+ diff --git a/sites/+components/tbl_communities.php b/sites/+components/tbl_communities.php index 98ecbe5..3183dd9 100644 --- a/sites/+components/tbl_communities.php +++ b/sites/+components/tbl_communities.php @@ -1,6 +1,7 @@ get_join_url()); $pubkey = html_sanitize($pubkey); $hostname = html_sanitize($hostname); + + $staff_json = json_encode(array_map('html_sanitize', $room->get_staff())); ?> - @@ -100,12 +104,18 @@ - Pictogram of a QR code + Pictogram of a QR code + Closed Groups - session.directory - Host Your Own Community - Session Terms Of Service - - - -
- Copied URL to clipboard. Paste into Session app to join -
+ >Closed Groups + session.directory + Host Your Own Community + Session Terms Of Service + + + +