diff --git a/.gitignore b/.gitignore index 08d9146c..4e4e3c3c 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 a57fb7fc..cee30a49 100644 --- a/.phpenv +++ b/.phpenv @@ -1,11 +1,12 @@ 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 COLUMN = { IDENTIFIER: 0, LANGUAGE: 1, NAME: 2, DESCRIPTION: 3, USERS: 4, PREVIEW: 5, @@ -38,6 +58,9 @@ export const ATTRIBUTES = { 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 26e48848..28bfbe00 100644 --- a/output/main.js +++ b/output/main.js @@ -16,7 +16,7 @@ // 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 } from './js/constants.js'; // Hidden communities for transparency. @@ -71,11 +71,46 @@ function onLoad() { } 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 = `qr-codes/${communityID}.png`; + + modal.showModal(); } function hideQRModal(communityID) { - dom.qr_modal(communityID).style.display = "none"; + dom.details_modal().close(); } function addQRModalHandlers() { @@ -87,13 +122,51 @@ function addQRModalHandlers() { 'click', () => displayQRModal(communityID) ); - const closeButton = - dom.qr_modal(communityID).querySelector('.qr-code-modal-close'); - closeButton.addEventListener( - 'click', - () => hideQRModal(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('data-staff')); + 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 createJoinLinkButtons() { @@ -131,13 +204,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. diff --git a/output/styles2.css b/output/styles2.css index caf0aaa1..3a982848 100644 --- a/output/styles2.css +++ b/output/styles2.css @@ -61,6 +61,10 @@ html:not(.js) .js-only { display: none; } +gap { + flex-grow: 1000; +} + header { display: flex; direction: row; @@ -355,23 +359,54 @@ label[for=toggle-show-room-ids]::after { } /* --- QR code modals --- */ -.qr-code { - display: block; - margin-left: auto; - margin-right: auto; - width: 50%; +#details-modal { + padding: 0; + max-width: 80vw; + max-height: 80vh; +} + +#details-modal-contents { + display: flex; + position: relative; + flex-direction: row; + padding: 2em; } -.qr-code-icon { + +#details-modal-close { + position: absolute; cursor: pointer; + top: 0.35em; + right: 0.5em; + font-size: 2em; +} + +#details-modal-start { + display: flex; + flex-direction: column; + margin-right: 1em; +} + +#details-modal-copy-button { + font-size: 1.1em; +} + +#details-modal-end { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; } + +#details-modal-end #details-modal-qr-code { + width: 20em; + height: 20em; +} + +#details-modal-end #details-modal-qr-code-label { + text-align: center; +} + .qr-code-modal { - display: none; /* Hidden by default */ - position: fixed; /* Stay in place */ - 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 */ diff --git a/php/utils/room-invites.php b/php/utils/room-invites.php new file mode 100644 index 00000000..8ec5bb53 --- /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 8d66b06b..4184fab9 100644 --- a/sites/+components/qr_modals.php +++ b/sites/+components/qr_modals.php @@ -1,46 +1,69 @@ 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); + require_once "$PROJECT_ROOT/php/utils/room-invites.php"; ?> -