1
0
Fork 1

Merge branch 'main' of lokilocker.com:SomeGuy/sessioncommunities.online

pull/15/head
mdPlusPlus 1 year ago
commit fbbf037ce7

@ -0,0 +1,59 @@
// This file contains definitions which help to reduce the amount
// of redunant values in the main file, especially those that could
// change in the foreseeable future.
export const dom = {
tbl_communities: () => document.getElementById("tbl_communities"),
td_last_checked: () => document.getElementById("td_last_checked"),
qr_modal: (communityID) => document.getElementById(`modal_${communityID}`),
join_urls: () => document.getElementsByClassName("td_join_url"),
td_summary: () => document.getElementById("td_summary"),
snackbar: () => document.getElementById("copy-snackbar")
}
export const COLUMN = {
IDENTIFIER: 0, LANGUAGE: 1, NAME: 2,
DESCRIPTION: 3, USERS: 4, PREVIEW: 5,
QR_CODE: 6, JOIN_URL: 7
};
// Reverse enum.
// Takes original key-value pairs, flips them, and casefolds the new values.
// Should correspond to #th_{} and .td_{} elements in communities table.
export const COLUMN_LITERAL = Object.fromEntries(
Object.entries(COLUMN).map(([name, id]) => [id, name.toLowerCase()])
);
export const COMPARISON = {
GREATER: 1, EQUAL: 0, SMALLER: -1
};
export const ATTRIBUTES = {
SORTING: {
ACTIVE: 'data-sort',
ASCENDING: 'data-sort-asc',
COLUMN: 'data-sorted-by',
COLUMN_LITERAL: 'sorted-by'
}
};
export function columnAscendingByDefault(column) {
return column != COLUMN.USERS;
}
export function columnIsSortable(column) { return column != COLUMN.QR_CODE; }
export function columnNeedsCasefold(column) {
return [
COLUMN.IDENTIFIER,
COLUMN.NAME,
COLUMN.DESCRIPTION
].includes(column);
}
export function columnIsNumeric(column) {
return [
COLUMN.USERS
].includes(column);
}

@ -0,0 +1,289 @@
// Hello reader!
// This project can be found at:
// https://lokilocker.com/someguy/sessioncommunities.online
/**
* This JavaScript file uses the JSDoc commenting style.
* Learn more: https://jsdoc.app/
*/
// Nudge TypeScript plugins to type-check using JSDoc comments.
// @ts-check
// Early prevention for bugs introduced by lazy coding.
'use strict';
// Import magic numbers and data
import {
dom, COLUMN, COLUMN_LITERAL, COMPARISON, ATTRIBUTES,
columnAscendingByDefault, columnIsSortable, columnNeedsCasefold,
columnIsNumeric
} from './js/constants.js';
// Hidden communities for transparency.
const filteredCommunities = {
tests: [
"2e9345+c7fb", // TestRoom
"762ba9+c7fb", // TesterRoom
"b4d829+c7fb", // Test
"e5853a+c7fb", // testtest
"fishing+8e2e", // Example group from PySOGS documentation
"test+118d", // Testing 1, 2, 3
"test+13f6", // Testing room
"test+c01b", // Testing room
"test+fe93", // 测试Test)
"xyz+efca", // XYZ Room
],
offensive: [
"60fa60+c7fb", // "N-word" Community
"ab1a4d+c7fb", // zUnsensored Group (CSAM)
"gore+e5e0" // gore
],
// These communities should be checked regularly
// in case they update their PySOGS version
legacy: [
"Ukraine+02bd" // https://reccacon.com/view/room/Ukraine
]
};
// This can be achieved with `text-overflow: ellipsis` instead
// and generated entirely server-side.
const transformJoinURL = (join_link) =>
`${join_link.substring(0, 31)}...
<button class="copy_button" onclick="copyToClipboard('${join_link}')">
Copy
</button>
`.trim();
function onLoad(timestamp) {
setLastChecked(timestamp);
hideBadCommunities();
sortTable(COLUMN.NAME);
createJoinLinkButtons();
markSortableColumns();
}
function displayQRModal(communityID) {
dom.qr_modal(communityID).style.display = "block";
}
function hideQRModal(communityID) {
dom.qr_modal(communityID).style.display = "none";
}
function createJoinLinkButtons() {
const join_URLs = dom.join_urls();
Array.from(join_URLs).forEach((td_url) => {
const a_href = td_url.querySelector('a'); // get first (only) <a> element
const join_link = a_href.getAttribute("href"); // get link
td_url.innerHTML = transformJoinURL(join_link); // add interactive content
});
}
function hideBadCommunities() {
let numberOfHiddenCommunities = 0;
for (const category of ['tests', 'offensive', 'legacy']) {
numberOfHiddenCommunities +=
filteredCommunities[category]
.map(hideElementByID)
.reduce((a, b) => a + b);
}
// Not ideal. Separate element should be allocated for content.
const summary = dom.td_summary();
summary.innerText += ` (${numberOfHiddenCommunities} hidden)`;
}
/**
* Removes an element by its ID and returns the number of elements removed.
*/
function hideElementByID(id) {
const element = document.getElementById(id);
element?.remove();
return element ? 1 : 0;
}
/**
* Copies text to clipboard and shows an informative toast.
* @param {string} text - Text to copy to clipboard.
*/
function copyToClipboard(text) {
navigator.clipboard.writeText(text);
// Find snackbar element
const snackbar = dom.snackbar();
snackbar.classList.add('show')
// After 3 seconds, hide the snackbar.
setTimeout(() => snackbar.classList.remove('show'), 3000);
}
/**
* Sets the "last checked indicator" based on a timestamp.
* @param {number} last_checked - Timestamp of last community list update.
*/
function setLastChecked(last_checked) {
const seconds_now = Math.floor(Date.now() / 1000); // timestamp in seconds
const time_passed_in_seconds = seconds_now - last_checked;
const time_passed_in_minutes =
Math.floor(time_passed_in_seconds / 60); // time in minutes, rounded down
const timestamp_element = dom.td_last_checked();
timestamp_element.innerText =
`Last checked ${time_passed_in_minutes} minutes ago.`;
}
/**
* Function comparing two elements.
*
* @callback comparer
* @param {*} fst - First value to compare.
* @param {*} snd - Second value to compare.
* @returns 1 if fst is to come first, -1 if snd is, 0 otherwise.
*/
/**
* Performs a comparison on two arbitrary values. Treats "" as Infinity.
* @param {*} fst - First value to compare.
* @param {*} snd - Second value to compare.
* @returns 1 if fst > snd, -1 if fst < snd, 0 otherwise.
*/
function compareAscending(fst, snd) {
// Triple equals to avoid "" == 0.
if (fst === "") return COMPARISON.GREATER;
if (snd === "") return COMPARISON.SMALLER;
return (fst > snd) - (fst < snd);
}
/**
* Performs a comparison on two arbitrary values. Treats "" as Infinity.
* @param {*} fst - First value to compare.
* @param {*} snd - Second value to compare.
* @returns -1 if fst > snd, 1 if fst < snd, 0 otherwise.
*/
function compareDescending(fst, snd) {
return -compareAscending(fst, snd);
}
/**
* Produces a comparer dependent on a derived property of the compared elements.
* @param {comparer} comparer - Callback comparing derived properties.
* @param {Function} getProp - Callback to retrieve derived property.
* @returns {comparer} Function comparing elements based on derived property.
*/
function compareProp(comparer, getProp) {
return (fst, snd) => comparer(getProp(fst), getProp(snd));
}
/**
* Produces a comparer for table rows based on given sorting parameters.
* @param {number} column - Numeric ID of column to be sorted.
* @param {boolean} ascending - Sort ascending if true, descending otherwise.
* @returns {comparer}
*/
function makeRowComparer(column, ascending) {
if (!columnIsSortable(column)) {
throw new Error(`Column ${column} is not sortable`);
}
// Callback to obtain sortable content from cell text.
let contentToSortable = (text) => text.trim();
if (columnNeedsCasefold(column)) {
// Make certain columns sort regardless of casing.
contentToSortable = (text) => text.toLowerCase().trim();
}
else if (columnIsNumeric(column)) {
// Make certain columns sort on parsed numeric value instead of text.
contentToSortable = (text) => parseInt(text);
}
// Construct comparer using derived property to determine sort order.
const rowComparer = compareProp(
ascending ? compareAscending : compareDescending,
row => contentToSortable(row.children[column].innerText)
);
return rowComparer;
}
/**
* @typedef {Object} SortState
* @property {number} column - Column ID being sorted.
* @property {boolean} ascending - Whether the column is sorted ascending.
*/
/**
* Retrieves a table's sort settings from the DOM.
* @param {HTMLElement} table - Table of communities being sorted.
* @returns {?SortState}
*/
function getSortState(table) {
if (!table.hasAttribute(ATTRIBUTES.SORTING.ACTIVE)) return null;
const directionState = table.getAttribute(ATTRIBUTES.SORTING.ASCENDING);
// This is not pretty, but the least annoying.
// Checking for classes would be more idiomatic.
const ascending = directionState.toString() === "true";
const columnState = table.getAttribute(ATTRIBUTES.SORTING.COLUMN);
const column = parseInt(columnState);
if (!Number.isInteger(column)) {
throw new Error(`Invalid column number read from table: ${columnState}`)
}
return { ascending, column };
}
/**
* Sets a table's sort settings using the DOM.
* @param {HTMLElement} table - Table of communities being sorted.
* @param {SortState} sortState - Sorting settings being applied.
*/
function setSortState(table, { ascending, column }) {
if (!table.hasAttribute(ATTRIBUTES.SORTING.ACTIVE)) {
table.setAttribute(ATTRIBUTES.SORTING.ACTIVE, true);
}
table.setAttribute(ATTRIBUTES.SORTING.ASCENDING, ascending);
table.setAttribute(ATTRIBUTES.SORTING.COLUMN, column);
// This can be used to style column headers in a consistent way, i.e.
// #tbl_communities[data-sort-asc=true][sorted-by=name]::after #th_name, ...
table.setAttribute(ATTRIBUTES.SORTING.COLUMN_LITERAL, COLUMN_LITERAL[column]);
}
// This is best done in JS, as it would require <noscript> styles otherwise.
function markSortableColumns() {
const table = dom.tbl_communities();
for (const th of table.querySelectorAll('th')) {
if (th.id.includes("qr_code")) continue;
th.classList.add('sortable');
}
}
/**
* Sorts the default communities table according the given column.
* Sort direction is determined by defaults; successive sorts
* on the same column reverse the sort direction.
* @param {number} column - Numeric ID of column being sorted.
*/
function sortTable(column) {
const table = dom.tbl_communities();
const sortState = getSortState(table);
const sortingNewColumn = column !== sortState?.column;
const ascending = sortingNewColumn
? columnAscendingByDefault(column)
: !sortState.ascending;
const compare = makeRowComparer(column, ascending);
const rows = Array.from(table.rows).slice(1);
rows.sort(compare);
rows.forEach((row) => row.remove());
table.querySelector("tbody").append(...rows);
setSortState(table, { ascending, column });
}
// Crude way to export from module script due to inline event handlers.
// Ideally, all handlers would be attached from JS via addEventListener.
Object.assign(window, {
onLoad, sortTable, displayQRModal,
hideQRModal, copyToClipboard
});

@ -1,154 +0,0 @@
var numberOfHiddenCommunities = 0;
function onLoad(timestamp) {
setLastChecked(timestamp);
hideBadCommunities();
sortTable(2); // 2 == Name
createJoinLinkButtons();
}
function displayQRModal(communityID) {
const modalID = "modal_" + communityID;
const modal = document.getElementById(modalID);
modal.style.display = "block";
}
function hideQRModal(communityID) {
const modalID = "modal_" + communityID;
const modal = document.getElementById(modalID);
modal.style.display = "none";
}
function createJoinLinkButtons() {
const elements = document.getElementsByClassName("td_join_url");
Array.from(elements).forEach((td_element) => {
const a_href = td_element.getElementsByTagName('a')[0]; // get first (only) <a> element
const join_link = a_href.getAttribute("href"); // get link
const cell_content = join_link.substring(0, 31) + "...<button class=\"copy_button\" onclick=\"copyToClipboard('" + join_link + "')\">Copy</button>";
td_element.innerHTML = cell_content;
});
}
function hideBadCommunities() {
const testCommunityIDs = [
"2e9345+c7fb", // TestRoom
"762ba9+c7fb", // TesterRoom
"b4d829+c7fb", // Test
"e5853a+c7fb", // testtest
"fishing+8e2e", // Example from here: https://github.com/oxen-io/session-pysogs/blob/0a5a58eb9f53e78f9ec93d2357cec3db18cefe79/administration.md
"test+118d", // Testing 1, 2, 3
"test+13f6", // Testing room
"test+c01b", // Testing room
"test+fe93", // 测试Test)
"xyz+efca", // XYZ Room
];
const badCommunityIDs = [
"60fa60+c7fb", // "N-word" Community
"ab1a4d+c7fb", // zUnsensored Group (CSAM)
"gore+e5e0" // gore
];
// These communities should be checked regularly in case they updated their PySOGS version
const legacyCommunitiyIDs = [
"Ukraine+02bd" // https://reccacon.com/view/room/Ukraine
];
testCommunityIDs.forEach(hideElementByID);
badCommunityIDs.forEach(hideElementByID);
legacyCommunitiyIDs.forEach(hideElementByID);
const summary = document.getElementById("td_summary");
summaryNew = summary.innerHTML + " (" + numberOfHiddenCommunities + " hidden)";
summary.innerHTML = summaryNew;
}
function hideElementByID(id) {
const element = document.getElementById(id);
if(element != null) {
element.remove();
numberOfHiddenCommunities++;
}
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text);
// Snackbar
const snackbar = document.getElementById("copy-snackbar");
// Add the "show" class to DIV
snackbar.className = "show";
// After 3 seconds, remove the show class from DIV
setTimeout(function(){ snackbar.className = snackbar.className.replace("show", ""); }, 3000);
}
function setLastChecked(timestamp) {
const now = Math.floor(Date.now() / 1000); // timestamp in seconds
const time_passed_in_seconds = now - timestamp;
const time_passed_in_minutes = Math.floor(time_passed_in_seconds / 60); // time in minutes, rounded down
const td_last_checked = document.getElementById("td_last_checked");
td_last_checked.innerHTML = "Last checked " + time_passed_in_minutes + " minutes ago.";
}
function sortTable(n) {
var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
table = document.getElementById("tbl_communities");
switching = true;
// Set the sorting direction to ascending:
dir = "asc";
// Make a loop that will continue until no switching has been done:
while (switching) {
// Start by saying: no switching is done:
switching = false;
rows = table.rows;
// Loop through all table rows (except the first, which contains table headers):
for (i = 1; i < (rows.length - 1); i++) {
// Start by saying there should be no switching:
shouldSwitch = false;
// Get the two elements you want to compare, one from current row and one from the next:
x = rows[i].getElementsByTagName("TD")[n];
y = rows[i + 1].getElementsByTagName("TD")[n];
// Check if the two rows should switch place, based on the direction, asc or desc:
if (dir == "asc") {
// If columns is users (4), sort numerically
if ( n == 4 ) {
if (Number(x.innerHTML) > Number(y.innerHTML)) {
shouldSwitch = true;
break;
}
} else if (x.innerHTML.toLowerCase() > y.innerHTML.toLowerCase()) {
// If so, mark as a switch and break the loop:
shouldSwitch = true;
break;
}
}
else if (dir == "desc") {
if ( n == 4 ) {
// If columns is users (4), sort numerically
if (Number(x.innerHTML) < Number(y.innerHTML)) {
shouldSwitch = true;
break;
}
} else if (x.innerHTML.toLowerCase() < y.innerHTML.toLowerCase()) {
// If so, mark as a switch and break the loop:
shouldSwitch = true;
break;
}
}
}
if (shouldSwitch) {
// If a switch has been marked, make the switch and mark that a switch has been done:
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
// Each time a switch is done, increase this count by 1:
switchcount ++;
} else {
// If no switching has been done AND the direction is "asc", set the direction to "desc" and run the while loop again.
if (switchcount == 0 && dir == "asc") {
dir = "desc";
switching = true;
}
}
}
}

@ -14,7 +14,7 @@
<meta charset="UTF-8">
<link rel="icon" type="image/svg+xml" href="favicon.svg" sizes="any">
<link rel="stylesheet" href="styles2.css">
<script src="script2.js" defer></script>
<script type="module" src="main.js" defer></script>
<title>Self-updating list of active Session communities</title>
</head>
<body onload="onLoad(<?php echo $timestamp ?>)">