feat: restructure site generation, reduce dom size

This commit does the following:
- Expands site generation with ability to include page-specific fragments
- Expands staff rating algorithms
- Restricts list of Communities statically included in main page
- Adds mechanism to fetch rest of Communities dynamically
- Adds /groups/all to request all pages statically
- Extracts duplicate tag information from servers.json into tags.json
- Minor changes (clickable h1 resets URL hash)
dev
gravel 4 months ago
parent c7b8c6d272
commit 606170e672
Signed by: gravel
GPG Key ID: C0538F3C906B308F

1
.gitignore vendored

@ -1,5 +1,6 @@
# Fetched room info
output/servers.json
output/tags.json
# Generated files
output/*.html

@ -91,6 +91,11 @@ a, .anchorstyle {
text-decoration: underline;
}
.non-anchorstyle {
color: inherit;
text-decoration: none;
}
.clickable {
cursor: pointer;
}

@ -4,12 +4,14 @@
class _RoomInfo {
static ROOMS_ENDPOINT = '/servers.json';
static TAGS_ENDPOINT = '/tags.json';
static rooms = {};
static servers = {};
static tags = {};
static async fetchRooms() {
const response = await fetch(this.ROOMS_ENDPOINT);
const servers = await response.json();
const responses = await Promise.all([fetch(this.ROOMS_ENDPOINT), fetch(this.TAGS_ENDPOINT)]);
const servers = await responses[0].json();
for (const server of servers) {
const { server_id } = server;
for (const room of server.rooms) {
@ -19,6 +21,7 @@ class _RoomInfo {
delete server.rooms;
this.servers[server_id] = server;
}
this.tags = await responses[1].json();
}
/**
@ -58,7 +61,11 @@ export class RoomInfo {
* @returns {{type: string, text: string, description: string}[]}
*/
static getRoomTags(identifier) {
return _RoomInfo.getRoom(identifier).tags;
const tags = _RoomInfo.getRoom(identifier).tags;
return tags.map(tag => ({
...tag,
description: tag.type == 'user' ? `Tag: ${tag.text}` : _RoomInfo.tags[tag.text]
}));
}
static getRoomStaff(identifier) {

@ -146,6 +146,14 @@ function addInformativeInteractions() {
});
}
async function fetchTableRestFragment() {
const request = await fetch("/_fragment/rest/");
const html = await request.text();
const tableBody = dom.tbl_communities()?.querySelector("tbody")
?? unreachable("could not find table body");
tableBody.innerHTML += html;
}
/**
* Triggers all actions dependent on page load.
*/
@ -155,6 +163,8 @@ async function onLoad() {
setLastChecked(timestamp);
}
hideBadCommunities();
await fetchTableRestFragment();
hideBadCommunities();
initializeSearch();
createJoinLinkUI();
markSortableColumns();
@ -434,9 +444,6 @@ function hideBadCommunities() {
.map(hideCommunity)
.reduce((a, b) => a + b);
}
const summary = dom.servers_hidden();
summary.innerText = `(${numberOfHiddenCommunities} hidden)`;
}
/**

@ -26,7 +26,7 @@
* 6. De-dupe servers based on pubkey
*/
function main() {
global $CACHE_ROOT, $ROOMS_FILE, $KNOWN_SERVERS, $KNOWN_PUBKEYS, $DO_DRY_RUN;
global $CACHE_ROOT, $ROOMS_FILE, $TAGS_FILE, $KNOWN_SERVERS, $KNOWN_PUBKEYS, $DO_DRY_RUN;
// Create default directories..
file_exists($CACHE_ROOT) or mkdir($CACHE_ROOT, 0700);
@ -65,7 +65,10 @@
);
// Output fetching results to file.
if (!$DO_DRY_RUN) file_put_contents($ROOMS_FILE, json_encode($servers));
if (!$DO_DRY_RUN) {
file_put_contents($ROOMS_FILE, json_encode($servers));
file_put_contents($TAGS_FILE, CommunityTag::serializeClassData());
}
}
// Fetch servers

@ -22,6 +22,13 @@
return $files;
}
function serialize_shell_environment(array $env_vars) {
$env_assignments = array_map(function(string $key, string $value) {
return "$key=".escapeshellarg($value);
}, array_keys($env_vars), array_values($env_vars));
return implode(' ', $env_assignments);
}
/**
* Generate files from PHP templates in the templates directory.
*/
@ -44,6 +51,7 @@
} else {
$docpath = str_replace(".php", ".html", $docpath);
}
$reldocpath = str_replace($DOCUMENT_ROOT, "", $docpath);
// We do this to isolate the environment and include-once triggers,
// otherwise we could include the documents in an ob_* wrapper.
@ -55,7 +63,13 @@
$exit_code = 0;
exec("cd '$TEMPLATES_ROOT'; php '$phppath' $flags", $output, $exit_code);
$env_vars = [
'SSG_TARGET' => $reldocpath,
];
$environment = serialize_shell_environment($env_vars);
exec("cd '$TEMPLATES_ROOT'; $environment php '$phppath' $flags", $output, $exit_code);
if ($exit_code != 0 || empty($output)) {
log_error("Site generation failed.");

@ -0,0 +1,34 @@
<?php
require_once "servers/servers-rooms.php";
class CommunityDatabase {
private function __construct(array $servers) {
$this->servers = $servers;
$this->rooms = CommunityServer::enumerate_rooms($servers);
}
/**
* @var CommunityServer[] $servers
*/
public array $servers;
/**
* @var CommunityRoom[] $rooms
*/
public array $rooms;
public static function read_from_file(string $rooms_file): CommunityDatabase {
$servers = CommunityServer::read_servers_from_file($rooms_file);
return new CommunityDatabase($servers);
}
public function fetch_assets(): CommunityDatabase {
CommunityRoom::fetch_assets($this->rooms);
return $this;
}
public function unpack() {
return [$this->rooms, $this->servers];
}
}
?>

@ -295,8 +295,13 @@
* @param CommunityRoom[] $rooms Rooms to sort by given key.
* @param string $key String property of CommunityRoom to sort by.
*/
public static function sort_rooms_str(array &$rooms, string $key) {
usort($rooms, function(CommunityRoom $a, CommunityRoom $b) use ($key) {
public static function sort_rooms_str(array &$rooms, string $key, bool $reverse = false) {
usort($rooms, $reverse ? function(CommunityRoom $a, CommunityRoom $b) use ($key) {
return strcmp(
$b->$key,
$a->$key
);
} : function(CommunityRoom $a, CommunityRoom $b) use ($key) {
return strcmp(
$a->$key,
$b->$key
@ -304,15 +309,37 @@
});
}
/**
* Sort Community rooms in-place by the given numeric property.
* @param CommunityRoom[] $rooms Rooms to sort by given key.
* @param string $key Numeric property of CommunityRoom to sort by.
*/
public static function sort_rooms_num(array &$rooms, string $key, bool $reverse = false) {
usort($rooms, $reverse ? function(CommunityRoom $a, CommunityRoom $b) use ($key) {
return $b->$key - $a->$key;
} : function(CommunityRoom $a, CommunityRoom $b) use ($key) {
return $a->$key - $b->$key;
});
}
/**
* Sort Community rooms in-place by their server.
* @param CommunityRoom[] $rooms Rooms to sort by server.
*/
public static function sort_rooms_by_server(array &$rooms) {
usort($rooms, function(CommunityRoom $a, CommunityRoom $b) {
public static function sort_rooms_by_server(array &$rooms, bool $random = false) {
if ($random) {
$servers = array_map(function(CommunityRoom $room) {
return $room->server;
}, $rooms);
shuffle($servers);
}
usort($rooms, $random ? function(CommunityRoom $a, CommunityRoom $b) use ($servers) {
return array_search($a->server, $servers) - array_search($b->server, $servers);
} : function(CommunityRoom $a, CommunityRoom $b) {
return strcmp(
$a->server->get_pubkey() . $a->server->get_hostname(),
$b->server->get_pubkey() . $b->server->get_hostname()
$a->server->get_server_sort_key(),
$b->server->get_server_sort_key()
);
});
}
@ -320,11 +347,26 @@
/**
* @param CommunityRoom[] $rooms
*/
public static function sort_stickied_rooms_first(array &$rooms) {
public static function fetch_assets(array $rooms) {
// Sequential in each server, see note in fetch_room_hints_coroutine()
$coroutines = [];
foreach ($rooms as $room) {
$coroutines[] = new FetchingCoroutine((function() use ($room) {
yield from fetch_qr_code_coroutine($room);
yield from fetch_room_icon_coroutine($room);
})());
}
(new FetchingCoroutineRunner($coroutines))->run_all();
}
/**
* @param CommunityRoom[] $rooms
*/
public static function get_stickied_rooms(array &$rooms, array &$rest = null) {
global $STICKIED_ROOMS;
usort($rooms, function(CommunityRoom $a, CommunityRoom $b) use ($STICKIED_ROOMS) {
return $b->matched_by_list($STICKIED_ROOMS) - $a->matched_by_list($STICKIED_ROOMS);
});
return CommunityRoom::select_rooms($rooms, $STICKIED_ROOMS, unmatched: $rest);
}
/**
@ -516,16 +558,23 @@
* @param string[] $matchees output parameter
* @return CommunityRoom[]
*/
public static function select_rooms(array $rooms, array|string $filter, array &$matchees = null): array {
public static function select_rooms(array $rooms, array|string $filter, array &$matchees = null, array &$unmatched = null): array {
$_matchees = [];
$rooms = array_values(array_filter($rooms, function(CommunityRoom $room) use ($filter, &$_matchees) {
$_unmatched = [];
$_rooms = [];
foreach ($rooms as $room) {
$matchee = null;
$success = $room->matched_by_list($filter, $matchee);
if ($success) $_matchees[] = $matchee;
return $success;
}));
if ($success) {
$_matchees[] = $matchee;
$_rooms[] = $room;
} else {
$_unmatched[] = $room;
}
};
$matchees = $_matchees;
return $rooms;
$unmatched = $_unmatched;
return $_rooms;
}
/**
@ -593,39 +642,36 @@
return 0;
}
/**
* Estimate for minimum number of users covered by one member of Community staff.
*/
private const USERS_PER_STAFF = 50;
/**
* Estimate for maximum number of users covered by one member of Community staff.
*/
private const USERS_PER_STAFF_WARNING = 200;
/**
* Number of minimum staff needed to moderate a Community.
*/
private const MINIMUM_STAFF = 2;
public function get_recommended_staff_count() {
if ($this->active_users == null || $this->active_users == 0) return INF;
$recommended_staff_count = ceil($this->active_users ** 0.25);
return max(2, $recommended_staff_count);
}
/**
* Estimate whether the Community has enough staff.
*/
private function has_good_staff_rating(): bool {
$recommended_staff_count = $this->active_users / CommunityRoom::USERS_PER_STAFF;
public function has_good_staff_rating(): bool {
$staff_count = count($this->get_staff());
return $staff_count >= $recommended_staff_count && $staff_count >= CommunityRoom::MINIMUM_STAFF;
return $staff_count >= $this->get_recommended_staff_count();
}
public function get_numeric_staff_rating() {
if (!$this->write || !$this->read) return 2;
return min(2, count($this->get_staff()) / $this->get_recommended_staff_count());
}
public function get_minimal_staff_count() {
if ($this->active_users == null || $this->active_users == 0) return INF;
$minimal_staff_count = 1 + round((0.38 * log($this->active_users)) ** 1.15);
return max(2, $minimal_staff_count);
}
/**
* Estimate whether the Community does not have enough staff.
*/
private function has_poor_staff_rating(): bool {
if ($this->active_users <= 3) {
return false;
}
$minimal_staff_count = $this->active_users / CommunityRoom::USERS_PER_STAFF_WARNING;
return count($this->get_staff()) < $minimal_staff_count;
public function has_poor_staff_rating(): bool {
return count($this->get_staff()) < $this->get_minimal_staff_count();
}
/**
@ -674,7 +720,7 @@
}
if ($this->write && $this->has_good_staff_rating()) {
$derived_tags[] = ReservedTags::moderated(CommunityRoom::USERS_PER_STAFF);
$derived_tags[] = ReservedTags::moderated();
}
if (!$this->write) {
@ -1272,6 +1318,10 @@
return $pubkey_prefix . $hostname_hash_prefix;
}
public function get_server_sort_key(): string {
return $this->get_pubkey() . $this->get_hostname();
}
/**
* Returns the room of the given token, or null if one does not exist.
*/
@ -1535,20 +1585,17 @@
}
/**
* @param CommunityServer[] $servers
* @return CommunityServer[]
*/
public static function fetch_assets(array $servers) {
// Sequential in each server, see note in fetch_room_hints_coroutine()
$coroutines = [];
public static function read_servers_from_file(string $file): array {
// Read the server data from disk.
$servers_raw = file_get_contents($file);
foreach (CommunityServer::enumerate_rooms($servers) as $room) {
$coroutines[] = new FetchingCoroutine((function() use ($room) {
yield from fetch_qr_code_coroutine($room);
yield from fetch_room_icon_coroutine($room);
})());
}
// Decode the server data to an associative array.
$server_data = json_decode($servers_raw, true);
(new FetchingCoroutineRunner($coroutines))->run_all();
// Re-build server instances from cached server data.
return CommunityServer::from_details_array($server_data);
}
/**

@ -33,16 +33,13 @@
* Create a new CommunityTag instance.
* @param string $text Text the tag should read.
* @param string $tag_type {@link TagType} enumeration value.
* @param string|null $description [optional] Brief explanation of tag.
*/
public function __construct(
string $text,
string $tag_type = TagType::USER_TAG,
?string $description = ""
) {
$this->text = CommunityTag::preprocess_tag($text);
$this->type = $tag_type;
$this->description = $description;
}
/**
@ -57,12 +54,6 @@
*/
public readonly string $text;
/**
* @var string $description
* A text description of the tag.
*/
public readonly string $description;
/**
* Return a lowercase representation of the tag for purposes of de-duping.
*/
@ -84,11 +75,31 @@
return html_sanitize($this->get_text());
}
public static $descriptions = [];
/**
* Return tag description.
*/
public function get_description_sanitized(): string {
return html_sanitize($this->description ?? "Tag: $this->text");
// Feels out-of-place anywhere else.
global $TAGS_FILE;
if (empty(CommunityTag::$descriptions)) {
CommunityTag::loadSerializedClassData(file_get_contents($TAGS_FILE));
}
return html_sanitize(CommunityTag::$descriptions[$this->text] ?? "Tag: $this->text");
}
public function set_description_globally(string $description): self {
CommunityTag::$descriptions[$this->text] = $description;
return $this;
}
public static function serializeClassData() {
return json_encode(CommunityTag::$descriptions);
}
public static function loadSerializedClassData(string $data) {
CommunityTag::$descriptions = json_decode($data, associative: true);
}
/**
@ -98,9 +109,6 @@
$details = [];
$details['text'] = $this->get_text();
$details['type'] = $this->get_tag_type();
if (!empty($this->description)) {
$details['description'] = $this->description;
}
return $details;
}
@ -175,8 +183,7 @@
public static function from_details(array $details): CommunityTag {
return new CommunityTag(
$details['text'],
$details['type'],
$details['description'] ?? ''
$details['type']
);
}
@ -278,90 +285,80 @@
public static function official() {
$CHECK_MARK = "✅";
return new CommunityTag(
return (new CommunityTag(
"official",
TagType::RESERVED_TAG,
"This Community is maintained by the Session team. $CHECK_MARK"
);
))->set_description_globally("This Community is maintained by the Session team. $CHECK_MARK");
}
public static function nsfw() {
$WARNING_ICON = "⚠️";
return new CommunityTag(
return (new CommunityTag(
"nsfw",
TagType::WARNING_TAG,
"This Community may contain adult material. $WARNING_ICON"
);
))->set_description_globally("This Community may contain adult material. $WARNING_ICON");
}
public static function moderated(int $users_per_staff = 0) {
public static function moderated() {
$CHECK_MARK = "✅";
return new CommunityTag(
return (new CommunityTag(
"moderated",
TagType::RESERVED_TAG,
"This Community has at least 1 staff per $users_per_staff active users. $CHECK_MARK"
);
))->set_description_globally("This Community seems to have enough moderators. $CHECK_MARK");
}
public static function not_modded(int $users_per_staff = 0) {
public static function not_modded() {
$WARNING_ICON = "⚠️";
return new CommunityTag(
return (new CommunityTag(
"not modded",
TagType::WARNING_TAG,
"This Community has less than 1 staff per $users_per_staff active users. $WARNING_ICON"
);
))->set_description_globally("This Community does not seem to have enough moderators. $WARNING_ICON");
}
public static function read_only() {
return new CommunityTag(
return (new CommunityTag(
"read-only",
TagType::RESERVED_TAG,
"This Community is read-only."
);
))->set_description_globally("This Community is read-only.");
}
public static function no_upload_permission() {
return new CommunityTag(
return (new CommunityTag(
"uploads off",
TagType::RESERVED_TAG,
"This Community does not support uploading files or link previews."
);
))->set_description_globally("This Community does not support uploading files or link previews.");
}
public static function recently_created() {
return new CommunityTag(
return (new CommunityTag(
"new",
TagType::RESERVED_TAG,
"This Community was created recently."
);
))->set_description_globally("This Community was created recently.");
}
public static function used_by_project() {
return new CommunityTag(
return (new CommunityTag(
"we're here",
TagType::RESERVED_TAG,
"The sessioncommunities.online maintainer(s) can post updates "
. "or respond to feedback in this Community."
);
))->set_description_globally("The sessioncommunities.online maintainer(s) can post updates "
. "or respond to feedback in this Community.");
}
public static function testing() {
return new CommunityTag(
return (new CommunityTag(
"test",
TagType::RESERVED_TAG,
"This Community is intended for testing only."
);
))->set_description_globally("This Community is intended for testing only.");
}
public static function stickied() {
return new CommunityTag(
return (new CommunityTag(
"pinned",
TagType::RESERVED_TAG,
"This Community has been pinned for greater visibility. 📌"
);
))->set_description_globally("This Community has been pinned for greater visibility. 📌");
}
}
?>

@ -0,0 +1,28 @@
<?php
class SiteGeneration {
public static function getCanonicalPageURL() {
global $SITE_CANONICAL_URL;
return dirname($SITE_CANONICAL_URL.getenv('SSG_TARGET')) . '/';
}
public static function getAbsoluteSourceDocumentPath() {
return $_SERVER['SCRIPT_NAME'];
}
public static function getTargetDocumentPath() {
return getenv('SSG_TARGET');
}
public static function getTargetDocumentRoute() {
return dirname(SiteGeneration::getTargetDocumentPath());
}
public static function getOwnSubDocumentPath(string $identifier) {
$page = SiteGeneration::getAbsoluteSourceDocumentPath();
$sub_document = dirname($page) . '/+' . preg_replace('/[.]php$/', ".$identifier.php", basename($page));
return $sub_document;
}
}
?>

@ -175,4 +175,8 @@
}
return htmlspecialchars($str, $flags, $encoding, $double_encode);
}
function sign(float $num): int {
return ($num > 0) - ($num < 0);
}
?>

@ -1,3 +1,4 @@
<?php require_once 'php/utils/site-generation.php'; ?>
<header>
<div id="header-start">
<a
@ -10,7 +11,22 @@
<a
href="#footer"
title="About"
>Jump to footer</a>
>More info</a>
<?php if (SiteGeneration::getTargetDocumentRoute() != '/groups/all'): ?>
<noscript>
<a
href="/groups/all/"
title="Full list of Communities"
>Full list</a>
</noscript>
<?php if (SiteGeneration::getTargetDocumentRoute() != '/'): ?>
<a
class="js-only"
href="/"
title="Main list of Communities"
>Main list</a>
<?php endif; ?>
<?php endif; ?>
</div>
<div id="header-end">
<label

@ -1,4 +1,8 @@
<?php
require_once 'php/utils/site-generation.php';
?>
<meta charset="UTF-8">
<link rel="canonical" href="<?=SiteGeneration::getCanonicalPageURL()?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/svg+xml" href="/favicon.svg" sizes="any">
<link rel="icon" type="image/ico" href="/favicon.ico" sizes="any">
@ -21,3 +25,7 @@
<meta property="og:image" content="/assets/og-image.webp"/>
<meta property="og:image:width" content="1200"/>
<meta property="og:image:height" content="630"/>
<meta property="og:url" content="<?=SiteGeneration::getCanonicalPageURL()?>">
<?php
include SiteGeneration::getOwnSubDocumentPath('head');
?>

@ -0,0 +1,134 @@
<?php
require_once 'php/utils/utils.php';
require_once 'php/servers/servers-rooms.php';
require_once 'php/assets/room-invites.php';
require_once 'php/assets/room-icons.php';
require_once 'php/assets/server-icons.php';
function renderCommunityRoomRow(CommunityRoom $room) {
if ($room->is_off_record()) {
// This can later allow SOGS
// to pass server-wide info using hidden dummy rooms.
return;
}
$pubkey = $room->server->get_pubkey();
$icon_hue = hexdec($pubkey[2] . $pubkey[2]);
$icon_color = "hsl($icon_hue, 80%, 50%)";
$server_icon = server_icon($room->server, '64x64');
$pubkey_shorthand = strtoupper($pubkey[0] . $pubkey[1]);
$id = html_sanitize($room->get_room_identifier());
$language = html_sanitize($room->get_language_flag());
$name = html_sanitize($room->name);
// $name_trunc = truncate($name, 16);
$name_trunc = "Community";
$desc = html_sanitize($room->description);
$users = html_sanitize($room->active_users);
$users_cutoff = html_sanitize($room->format_user_cutoff_period());
$users_tooltip = $room->read
? "$users active users in the last $users_cutoff"
: "$users users with read privileges, ??? others";
$users = $room->read ? $users : "—";
$preview_link = html_sanitize($room->get_preview_url());
$join_link = html_sanitize($room->get_join_url());
$pubkey = html_sanitize($pubkey);
$hostname = html_sanitize($room->server->get_hostname());
$class_list = ["room-row"];
if ($room->is_stickied_room()) {
$class_list[] = "room-row-stickied";
}
$classname = implode(" ", $class_list);
/**
* Note on refactoring:
* Icon is hard to move to JSON because it'd have to be generated by fetching code
* Icon safety is depended on by CSS styles
*/
?>
<tr class="<?=$classname?>"
itemscope
itemtype="https://schema.org/EntryPoint"
data-id="<?=$id?>"
data-icon='<?=room_icon($room, '64x64')?>:<?=$room->icon_safety()?>'
>
<td class="td_language" title="Language flag for '<?=$name_trunc?>'"><?=$language?></td>
<td class="td_name">
<a
href="<?=$preview_link?>"
target="_blank"
title="Click here to preview <?=$name_trunc?>"
rel="noopener noreferrer external nofollow"
itemprop="url"
><span itemprop="name"><?=
$name
?></span></a>
<span><?php /* class="tags-container" */ ?>
<?php foreach ($room->get_showcased_room_tags() as $tag): ?>
<span
class="tag <?=$tag->get_tag_classname()?> badge"
title="<?=$tag->get_description_sanitized()?>"
><?=
truncate($tag->get_text_sanitized(), 16)
?></span>
<?php endforeach; ?>
</span>
</td>
<td
class="td_description"
title="Description"
itemprop="description"
><?=$desc?></td>
<td
class="td_users"
title="<?=$users_tooltip?>."
><?=$users?></td>
<td class="td_preview">
<a
href="<?=$preview_link?>"
title="Preview <?=$name_trunc?>"
target="_blank"
rel="noopener noreferrer external nofollow"
>
<span></span>
</a>
</td>
<td class="td_qr_code">
<a
href="<?=room_qr_code($room)?>"
target="_blank"
title="Click here to view details for <?=$name_trunc?>"
>
<div></div>
</a>
</td>
<td class="td_server_icon"
title="Host: <?=$hostname?>"
>
<?php if (empty($server_icon)): ?>
<div class="td_server_icon-circle" style="background-color: <?=$icon_color?>">
<span class="td_server_icon-text"><?=$pubkey_shorthand?></span>
</div>
<?php else: ?>
<div class="td_server_icon-circle" style="background-image: url('<?=$server_icon?>')"></div>
<?php endif; ?>
</td>
<td class="td_join_url">
<div>
<span></span><?php /* Join URL preview */ ?>
<a
class="noscript"
href="<?=$join_link?>"
title="Right click -> Copy link"
rel="external nofollow"
>Copy this</a>
</div>
</td>
</tr>
<?php
}
?>

@ -0,0 +1,12 @@
<?php
require_once 'community-row.php';
/**
* @param CommunityRoom[] $rooms
*/
function renderCommunityRoomTableFragment(array $rooms) {
foreach ($rooms as $room) {
renderCommunityRoomRow($room);
}
}
?>

@ -1,182 +1,51 @@
<?php
require_once 'php/utils/utils.php';
require_once 'php/servers/servers-rooms.php';
require_once 'php/assets/room-invites.php';
require_once 'php/assets/room-icons.php';
require_once 'php/assets/server-icons.php';
require_once '+components/table/table-fragment.php';
/**
* @var CommunityRoom[] $rooms
* @param CommunityRoom[] $rooms
*/
function renderCommunityRoomTable(array $rooms) {
// Once handlers are attached in JS, this check ceases to be useful.
function column_sortable($id) {
// Join URL contents are not guaranteed to have visible text.
return $id != "qr_code" && $id != "preview" && $id != "join_url";
}
function sort_onclick($colno) {
global $TABLE_COLUMNS;
$column = $TABLE_COLUMNS[$colno];
$name = isset($column['name_long']) ? $column['name_long'] : $column['name'];
if (!column_sortable($column['id'])) return " title='$name'";
return " title='Click to sort by $name.'";
}
// Note: Changing the names or columns displayed requires updating
// the --expanded-static-column-width and --collapsed-static-column-width CSS variables.
$TABLE_COLUMNS = [
['id' => "language", 'name' => "L", 'name_long' => "Language"],
['id' => "name", 'name' => "Name"],
['id' => "description", 'name' => "About", 'name_long' => "Description"],
['id' => "users", 'name' => "#", 'name_long' => "Active Users"],
['id' => "preview", 'name' => "Preview"],
['id' => "qr_code", 'name' => "QR", 'name_long' => "QR Code (for use in-app)"],
['id' => "server_icon", 'name' => "Host", 'name_long' => "Server host"],
['id' => "join_url", 'name' => "URL", 'name_long' => "Join URL (for use in-app)"],
];
$SERVER_ICON_COLUMN = array_keys(array_filter($TABLE_COLUMNS, function($column){return $column['id'] == "server_icon";}))[0];
?>
<table id="tbl_communities" data-sort="true" data-sort-asc="true" data-sorted-by="<?=$SERVER_ICON_COLUMN?>">
<tr>
<?php foreach ($TABLE_COLUMNS as $colno => $column): ?>
<th<?=sort_onclick($colno)?> id="th_<?=$column['id']?>" class="tbl_communities__th">
<?=$column['name']?>
</th>
<?php endforeach; ?>
</tr>
<?php foreach ($rooms as $id => $room): ?>
<?php
/**
* @var CommunityRoom $room
*/
if ($room->is_off_record()) {
// This can later allow SOGS
// to pass server-wide info using hidden dummy rooms.
continue;
// Once handlers are attached in JS, this check ceases to be useful.
function column_sortable($id) {
// Join URL contents are not guaranteed to have visible text.
return $id != "qr_code" && $id != "preview" && $id != "join_url";
}
$pubkey = $room->server->get_pubkey();
$icon_hue = hexdec($pubkey[2] . $pubkey[2]);
$icon_color = "hsl($icon_hue, 80%, 50%)";
$server_icon = server_icon($room->server, '64x64');
$pubkey_shorthand = strtoupper($pubkey[0] . $pubkey[1]);
$id = html_sanitize($room->get_room_identifier());
$language = html_sanitize($room->get_language_flag());
$name = html_sanitize($room->name);
// $name_trunc = truncate($name, 16);
$name_trunc = "Community";
$desc = html_sanitize($room->description);
$users = html_sanitize($room->active_users);
$users_cutoff = html_sanitize($room->format_user_cutoff_period());
$users_tooltip = $room->read
? "$users active users in the last $users_cutoff"
: "$users users with read privileges, ??? others";
$users = $room->read ? $users : "—";
$preview_link = html_sanitize($room->get_preview_url());
$join_link = html_sanitize($room->get_join_url());
$pubkey = html_sanitize($pubkey);
$hostname = html_sanitize($room->server->get_hostname());
$class_list = ["room-row"];
if ($room->is_stickied_room()) {
$class_list[] = "room-row-stickied";
function sort_onclick($colno) {
global $TABLE_COLUMNS;
$column = $TABLE_COLUMNS[$colno];
$name = isset($column['name_long']) ? $column['name_long'] : $column['name'];
if (!column_sortable($column['id'])) return " title='$name'";
return " title='Click to sort by $name.'";
}
$classname = implode(" ", $class_list);
/**
* Note on refactoring:
* Icon is hard to move to JSON because it'd have to be generated by fetching code
* Icon safety is depended on by CSS styles
*/
// Note: Changing the names or columns displayed requires updating
// the --expanded-static-column-width and --collapsed-static-column-width CSS variables.
$TABLE_COLUMNS = [
['id' => "language", 'name' => "L", 'name_long' => "Language"],
['id' => "name", 'name' => "Name"],
['id' => "description", 'name' => "About", 'name_long' => "Description"],
['id' => "users", 'name' => "#", 'name_long' => "Active Users"],
['id' => "preview", 'name' => "Preview"],
['id' => "qr_code", 'name' => "QR", 'name_long' => "QR Code (for use in-app)"],
['id' => "server_icon", 'name' => "Host", 'name_long' => "Server host"],
['id' => "join_url", 'name' => "URL", 'name_long' => "Join URL (for use in-app)"],
];
?>
<tr class="<?=$classname?>"
itemscope
itemtype="https://schema.org/EntryPoint"
data-id="<?=$id?>"
data-icon='<?=room_icon($room, '64x64')?>:<?=$room->icon_safety()?>'
>
<td class="td_language" title="Language flag for '<?=$name_trunc?>'"><?=$language?></td>
<td class="td_name">
<a
href="<?=$preview_link?>"
target="_blank"
title="Click here to preview <?=$name_trunc?>"
rel="noopener noreferrer external nofollow"
itemprop="url"
><span itemprop="name"><?=
$name
?></span></a>
<span><?php /* class="tags-container" */ ?>
<?php foreach ($room->get_showcased_room_tags() as $tag): ?>
<span
class="tag <?=$tag->get_tag_classname()?> badge"
title="<?=$tag->get_description_sanitized()?>"
><?=
truncate($tag->get_text_sanitized(), 16)
?></span>
<?php endforeach; ?>
</span>
</td>
<td
class="td_description"
title="Description"
itemprop="description"
><?=$desc?></td>
<td
class="td_users"
title="<?=$users_tooltip?>."
><?=$users?></td>
<td class="td_preview">
<a
href="<?=$preview_link?>"
title="Preview <?=$name_trunc?>"
target="_blank"
rel="noopener noreferrer external nofollow"
>
<span></span>
</a>
</td>
<td class="td_qr_code">
<a
href="<?=room_qr_code($room)?>"
target="_blank"
title="Click here to view details for <?=$name_trunc?>"
>
<div></div>
</a>
</td>
<td class="td_server_icon"
title="Host: <?=$hostname?>"
>
<?php if (empty($server_icon)): ?>
<div class="td_server_icon-circle" style="background-color: <?=$icon_color?>">
<span class="td_server_icon-text"><?=$pubkey_shorthand?></span>
</div>
<?php else: ?>
<div class="td_server_icon-circle" style="background-image: url('<?=$server_icon?>')"></div>
<?php endif; ?>
</td>
<td class="td_join_url">
<div>
<span></span><?php /* Join URL preview */ ?>
<a
class="noscript"
href="<?=$join_link?>"
title="Right click -> Copy link"
rel="external nofollow"
>Copy this</a>
</div>
</td>
</tr>
<table id="tbl_communities">
<tr>
<?php foreach ($TABLE_COLUMNS as $colno => $column): ?>
<th<?=sort_onclick($colno)?> id="th_<?=$column['id']?>" class="tbl_communities__th">
<?=$column['name']?>
</th>
<?php endforeach; ?>
</table>
</tr>
<?php renderCommunityRoomTableFragment($rooms); ?>
</table>
<?php
}
?>

@ -0,0 +1 @@
<h1 id="headline"><span id="headline-01">Session</span><span id="headline-02">Communities</span><span id="headline-03">.online</span></h1>

@ -0,0 +1,13 @@
<title>Session Communities List — sessioncommunities.online</title>
<meta name="description" content="Chat in Session Communities! <?php
?>Use our list to join your favorite Community in Session Messenger. <?php
?>Copy public Session group links into the Session app!<?php
?>">
<meta name="keywords" content="session communities,session groups,session group list,session chat">
<meta property="og:title" content="Session Communities — Come chat!">
<meta
property="og:description"
content="100+ Session Communities."
>
<meta property="og:type" content="website">
<meta property="og:locale" content="en_US"/>

@ -0,0 +1,133 @@
<?php
require_once 'php/utils/utils.php';
require_once 'php/utils/site-generation.php';
require_once 'php/servers/servers-rooms.php';
require_once '+components/tbl-communities.php';
// Set the last-updated timestamp
// to the time the server data file was last modified.
$time_modified = filemtime($ROOMS_FILE);
$time_modified_str = date("Y-m-d H:i:s", $time_modified);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<?php include "+components/page-head.php" ?>
<meta name="modified" content="<?=$time_modified_str?>">
<meta name="timestamp" content="<?=$time_modified?>">
<link rel="stylesheet" href="/index.css?<?=md5_file("$DOCUMENT_ROOT/index.css")?>">
<link rel="stylesheet" href="/css/banner.css?<?=md5_file("$DOCUMENT_ROOT/css/banner.css")?>">
<script type="module" src="/main.js?<?=md5_file("$DOCUMENT_ROOT/main.js")?>"></script>
<link rel="modulepreload" href="/js/util.js">
<link rel="preload" href="/servers.json" as="fetch" crossorigin="anonymous"/>
<link rel="preload" href="/tags.json" as="fetch" crossorigin="anonymous"/>
<link rel="help" href="/instructions/">
<noscript>
<style>
.js-only {
display: none;
}
</style>
</noscript>
<?php include "+components/communities-json-ld.php"; ?>
</head>
<body>
<input type="checkbox" id="toggle-theme-switch">
<div id="theming-root">
<?php include "+components/index-header.php" ?>
<a
href="#"
class="non-anchorstyle"
><?php include SiteGeneration::getOwnSubDocumentPath('h1'); ?></a>
<?php include "+components/issue-banner.php" ?>
<?php include "+components/communities-search.php"; ?>
<?php include "+components/qr-modals.php" ?>
<?php renderCommunityRoomTable($rooms); ?>
<gap></gap>
<hr id="footer-divider">
<aside id="summary" itemid="<?=$SITE_CANONICAL_URL?>" itemtype="https://schema.org/WebSite">
<p id="server_summary">
<?=count($room_database->rooms)?> unique Session Communities
on <?=count($room_database->servers)?> servers have been found.
<?php if (SiteGeneration::getTargetDocumentRoute() == '/'): ?>
<noscript>
<span>
(Viewing <?=count($rooms)?>;
<a
href="/groups/all"
title="Full list of Communities"
>full list</a>.)
</span>
</noscript>
<?php endif; ?>
</p>
<p id="last_checked">
Last checked <span id="last_checked_value" itemprop="dateModified" value="<?=$time_modified_str?>">
<?=$time_modified_str?> (UTC)
</span>.
</p>
</aside>
<aside id="details">
<details>
<summary class="carousel-label aside__h2 h2-like">What is Session Messenger?</summary>
<p class="carousel-target">
<a href="https://getsession.org/" rel="external">Session</a>
is a private messaging app that protects your meta-data,
encrypts your communications, and makes sure your messaging activities
leave no digital trail behind. <a href="/about/" title="About page">Read more.</a>
</p>
</details>
<details>
<summary class="carousel-label aside__h2 h2-like">What are Session Communities?</summary>
<p class="carousel-target">
Session Communities are public Session chat rooms accessible from within Session Messenger.
This web project crawls known sources of Session Communities, and
displays information about them as a static HTML page. <a href="/about/" title="About page">Read more.</a>
</p>
</details>
<p>Disclaimer:</p>
<p id="content-disclaimer">
Session chat rooms shown on this list are fetched automatically from
<a
href="<?=$REPOSITORY_CANONICAL_URL?>#which-sources-are-crawled"
target="_blank"
>various sources</a>.
<br>
<span class="js-only">
We make an attempt to hide Communities containing
objectionable or illegal content, but
you should still proceed with caution.
</span>
<noscript>
<span>
Proceed with caution when joining unofficial Communities.
As JavaScript is disabled, no Communities are filtered from the list.
</span>
</noscript>
</p>
<noscript>
<p>
SessionCommunities.online works fine without JavaScript.
However, some interactive features are
only available with JS enabled.
</p>
</noscript>
</aside>
<?php include "+components/footer.php"; ?>
<div id="copy-snackbar"></div>
</div>
</body>
</html>

@ -0,0 +1,116 @@
<?php
require_once 'php/servers/servers-rooms.php';
require_once 'php/utils/utils.php';
class RoomSieve {
/**
* @var CommunityRoom[] $rooms;
*/
private array $rooms;
/**
* @var CommunityRoom[] $stickied
*/
private array $stickies;
/**
* @param CommunityRoom[] $rooms
* @param CommunityRoom[] $stickied
*/
private function __construct(array $rooms, array $stickied = []) {
$this->rooms = $rooms;
$this->stickies = $stickied;
}
public const TOP_DEFAULT = 35;
/**
* @param CommunityRoom[] $rooms
*/
public static function takeRooms(array $rooms) {
return new RoomSieve($rooms);
}
public function saveStickies() {
$stickied = CommunityRoom::get_stickied_rooms($this->rooms, $rest);
$rooms = $rest;
return new RoomSieve($rooms, $stickied);
}
/**
* @param CommunityRoom[] $rooms
*/
public function addRooms(array $rooms) {
return new RoomSieve(array_merge($this->rooms, $rooms), $this->stickies);
}
public function apply(Closure $filter) {
return new RoomSieve($filter($this->rooms), $this->stickies);
}
public function getWithStickies() {
return [...$this->stickies, ...$this->rooms];
}
public function getWithoutStickies() {
return $this->saveStickies()->getRooms();
}
public function getRooms() {
return $this->rooms;
}
public function onlyTop(int $count = RoomSieve::TOP_DEFAULT) {
$rooms = $this->rooms;
return new RoomSieve(array_slice(array_reverse($rooms), 0, $count), $this->stickies);
}
public function exceptTop(int $count = RoomSieve::TOP_DEFAULT) {
$rooms = $this->rooms;
CommunityRoom::sort_rooms_num($rooms, 'active_users');
return new RoomSieve(array_slice(array_reverse($rooms), $count), $this->stickies);
}
private static function isIndexApproved(CommunityRoom $room): bool {
return (
!$room->rated_nsfw() &&
$room->write &&
!$room->has_poor_staff_rating() &&
!empty($room->description)
);
}
public function applyStandardSort() {
$rooms = $this->rooms;
CommunityRoom::sort_rooms_str($rooms, 'name');
CommunityRoom::sort_rooms_by_server($rooms);
return new RoomSieve($rooms, $this->stickies);
}
public function applyPreferentialSort() {
$rooms = $this->rooms;
CommunityRoom::sort_rooms_num($rooms,'created');
usort($rooms, function($a, $b) {
return empty($b->description) - empty($a->description);
});
usort($rooms, function(CommunityRoom $a, CommunityRoom $b) {
return sign($a->get_numeric_staff_rating() - $b->get_numeric_staff_rating());
});
return new RoomSieve(array_reverse($rooms), $this->stickies);
}
public function indexApproved() {
$rooms = array_values(array_filter($this->rooms, function($room) {
return RoomSieve::isIndexApproved($room);
}));
return new RoomSieve($rooms, $this->stickies);
}
public function indexNonApproved() {
$rooms = array_values(array_filter($this->rooms, function($room) {
return !RoomSieve::isIndexApproved($room);
}));
return new RoomSieve($rooms, $this->stickies);
}
}
?>

@ -0,0 +1,22 @@
<?php
require_once '+getenv.php';
require_once 'php/utils/getopt.php';
require_once 'php/servers/room-database.php';
require_once 'sites/_fragment/+room-sieve.php';
require_once "sites/+components/table/table-fragment.php";
$room_database = CommunityDatabase::read_from_file($ROOMS_FILE)->fetch_assets();
$rooms =
RoomSieve::takeRooms($room_database->rooms)
->indexApproved()
->exceptTop()
->addRooms(
RoomSieve::takeRooms($room_database->rooms)
->indexNonApproved()
->getWithoutStickies()
)
->applyPreferentialSort()
->getWithoutStickies();
renderCommunityRoomTableFragment($rooms);
?>

@ -1,5 +1,5 @@
<?php
// prerequisite include for sites and components
// prerequisite include for sites
require_once '+getenv.php';
?>
<!DOCTYPE html>
@ -7,7 +7,7 @@
<head>
<?php include('+components/page-head.php'); ?>
<link rel="canonical" href="<?=$SITE_CANONICAL_URL?>/about/">
<link rel="stylesheet" href="/css/common-dark.css">
<meta name="description" content="Learn more about sessioncommunities.online, the up-to-date list of public Session groups; learn how to get your Session Community listed or how to contact us.">
<title>About — sessioncommunities.online</title>
@ -16,7 +16,7 @@
?>This web project crawls known sources of Session Communities, and <?php
?>displays information about them as a static HTML page.">
<meta property="og:type" content="article">
<meta property="og:url" content="<?=$SITE_CANONICAL_URL?>/about/">
</head>
<body>
<h1 id="self-updating-list-of-session-communities"><a href="/" title="Visit sessioncommunities.online">Session Community List</a> — About</h1>

@ -6,13 +6,13 @@
<head>
<?php include "+components/page-head.php" ?>
<link rel="canonical" href="<?=$SITE_CANONICAL_URL?>/404/">
<title>404 Not Found — sessioncommunities.online</title>
<meta name="description" content="404 — Not Found page for SessionCommunities.online">
<meta property="og:title" content="Privacy — sessioncommunities.online">
<meta property="og:description" content="Read our transparent account of what data sessioncommunities.online collects when you browse the site.">
<meta property="og:type" content="article">
<meta property="og:url" content="<?=$SITE_CANONICAL_URL?>/404/">
<link rel="stylesheet" href="/css/common-dark.css">
<script src="/js/404.js" async></script>
</head>

@ -0,0 +1 @@
<h1 id="headline">Full list of Session Communities</h1>

@ -0,0 +1,13 @@
<title>All Communities — sessioncommunities.online</title>
<meta name="description" content="Chat in Session Communities! <?php
?>Use our list to join your favorite Community in Session Messenger. <?php
?>Copy public Session group links into the Session app!<?php
?>">
<meta name="keywords" content="session communities,session groups,session group list,session chat">
<meta property="og:title" content="Session Communities — Come chat!">
<meta
property="og:description"
content="100+ Session Communities."
>
<meta property="og:type" content="website">
<meta property="og:locale" content="en_US"/>

@ -0,0 +1,15 @@
<?php
require_once '+getenv.php';
require_once 'php/utils/getopt.php';
require_once 'php/servers/room-database.php';
require_once 'sites/_fragment/+room-sieve.php';
$room_database = CommunityDatabase::read_from_file($ROOMS_FILE)->fetch_assets();
$rooms =
RoomSieve::takeRooms($room_database->rooms)
->saveStickies()
->applyStandardSort()
->getWithStickies();
include "+templates/index.php";
?>

@ -1,155 +1,18 @@
<?php
// prerequisite include for sites and components
require_once '+getenv.php';
require_once 'php/utils/getopt.php';
require_once 'php/utils/utils.php';
require_once 'php/servers/servers-rooms.php';
// Read the server data from disk.
$servers_raw = file_get_contents($ROOMS_FILE);
// Decode the server data to an associative array.
$server_data = json_decode($servers_raw, true);
// Re-build server instances from cached server data.
$servers = CommunityServer::from_details_array($server_data);
// Fetch all server assets ahead of time.
CommunityServer::fetch_assets($servers);
// List all rooms from the cached servers.
$rooms = CommunityServer::enumerate_rooms($servers);
// Sort rooms by name and then host.
CommunityRoom::sort_rooms_str($rooms, 'name');
CommunityRoom::sort_rooms_by_server($rooms);
CommunityRoom::sort_stickied_rooms_first($rooms);
// Set the last-updated timestamp
// to the time the server data file was last modified.
$time_modified = filemtime($ROOMS_FILE);
$time_modified_str = date("Y-m-d H:i:s", $time_modified);
require_once 'php/servers/room-database.php';
require_once 'sites/_fragment/+room-sieve.php';
$room_database = CommunityDatabase::read_from_file($ROOMS_FILE)->fetch_assets();
$rooms =
RoomSieve::takeRooms($room_database->rooms)
->saveStickies()
->indexApproved()
->onlyTop()
->applyStandardSort()
->applyPreferentialSort()
->getWithStickies();
include '+templates/index.php';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<?php include "+components/page-head.php" ?>
<link rel="canonical" href="<?=$SITE_CANONICAL_URL?>/">
<link rel="stylesheet" href="/index.css?<?=md5_file("$DOCUMENT_ROOT/index.css")?>">
<link rel="stylesheet" href="/css/banner.css?<?=md5_file("$DOCUMENT_ROOT/css/banner.css")?>">
<script type="module" src="/main.js?<?=md5_file("$DOCUMENT_ROOT/main.js")?>"></script>
<link rel="modulepreload" href="/js/util.js">
<link rel="preload" href="/servers.json" as="fetch" crossorigin="anonymous"/>
<link rel="help" href="/instructions/">
<title>Session Communities List — sessioncommunities.online</title>
<meta name="description" content="Chat in Session Communities! <?php
?>Use our list to join your favorite Community in Session Messenger. <?php
?>Copy public Session group links into the Session app!<?php
?>">
<meta name="keywords" content="session communities,session groups,session group list,session chat">
<meta name="modified" content="<?=$time_modified_str?>">
<meta property="og:title" content="Session Communities — Come chat!">
<meta
property="og:description"
content="<?=count($rooms)?> Communities and counting."
>
<meta property="og:url" content="<?=$SITE_CANONICAL_URL?>/">
<meta property="og:type" content="website">
<meta property="og:locale" content="en_US"/>
<meta name="timestamp" content="<?=$time_modified?>">
<noscript>
<style>
.js-only {
display: none;
}
</style>
</noscript>
<?php include "+components/communities-json-ld.php"; ?>
</head>
<body>
<input type="checkbox" id="toggle-theme-switch">
<div id="theming-root">
<?php include "+components/index-header.php" ?>
<h1 id="headline"><span id="headline-01">Session</span><span id="headline-02">Communities</span><span id="headline-03">.online</span></h1>
<?php include "+components/issue-banner.php" ?>
<?php include "+components/communities-search.php"; ?>
<?php include "+components/qr-modals.php" ?>
<?php include "+components/tbl-communities.php" ?>
<gap></gap>
<hr id="footer-divider">
<aside id="summary" itemid="<?=$SITE_CANONICAL_URL?>" itemtype="https://schema.org/WebSite">
<p id="server_summary">
<?=count_rooms($servers)?> unique Session Communities
on <?=count($servers)?> servers have been found.
<span id="servers_hidden">(None hidden as JS is off)</span>
<sup><a
href="<?=$REPOSITORY_CANONICAL_URL?>/#policy"
title="SessionCommunities.Online policy"
target="_blank"
>why?</a></sup>
</p>
<p id="last_checked">
Last checked <span id="last_checked_value" itemprop="dateModified" value="<?=$time_modified_str?>">
<?=$time_modified_str?> (UTC)
</span>.
</p>
</aside>
<aside id="details">
<details>
<summary class="carousel-label aside__h2 h2-like">What is Session Messenger?</summary>
<p class="carousel-target">
<a href="https://getsession.org/" rel="external">Session</a>
is a private messaging app that protects your meta-data,
encrypts your communications, and makes sure your messaging activities
leave no digital trail behind. <a href="/about/" title="About page">Read more.</a>
</p>
</details>
<details>
<summary class="carousel-label aside__h2 h2-like">What are Session Communities?</summary>
<p class="carousel-target">
Session Communities are public Session chat rooms accessible from within Session Messenger.
This web project crawls known sources of Session Communities, and
displays information about them as a static HTML page. <a href="/about/" title="About page">Read more.</a>
</p>
</details>
<p>Disclaimer:</p>
<p id="content-disclaimer">
Session chat rooms shown on this list are fetched automatically from
<a
href="<?=$REPOSITORY_CANONICAL_URL?>#which-sources-are-crawled"
target="_blank"
>various sources</a>.
<br>
<span class="js-only">
We make an attempt to hide Communities containing
objectionable or illegal content, but
you should still proceed with caution.
</span>
<span class="noscript">
Proceed with caution when joining unofficial Communities.
As JavaScript is disabled, no Communities are filtered from the list.
</span>
</p>
<p class="noscript">
SessionCommunities.online works fine without JavaScript.
However, some interactive features are
only available with JS enabled.
</p>
</aside>
<?php include "+components/footer.php"; ?>
<div id="copy-snackbar"></div>
</div>
</body>
</html>

@ -20,7 +20,7 @@
<head>
<?php include "+components/page-head.php" ?>
<link rel="canonical" href="<?=$SITE_CANONICAL_URL?>/instructions/">
<link rel="stylesheet" href="/css/common-dark.css">
<link rel="stylesheet" href="/css/instructions.css">
<style type="text/css">
@ -38,7 +38,7 @@
?>">
<meta property="og:title" content="How to — sessioncommunities.online">
<meta property="og:description" content="Learn how to use sessioncommunities.online to join Communities in Session Messenger.">
<meta property="og:url" content="<?=$SITE_CANONICAL_URL?>/instructions/">
<meta property="og:type" content="article">
<title>Instructions — sessioncommunities.online</title>
</head>

@ -12,7 +12,7 @@
<head>
<?php include "+components/page-head.php" ?>
<link rel="canonical" href="<?=$SITE_CANONICAL_URL?>/privacy/">
<title>Privacy — sessioncommunities.online</title>
<meta name="description" content="<?php
?>This page covers the Privacy Policy of sessioncommunities.online <?php
@ -20,7 +20,7 @@
<meta property="og:title" content="Privacy — sessioncommunities.online">
<meta property="og:description" content="Read our transparent account of what data sessioncommunities.online collects when you browse the site.">
<meta property="og:type" content="article">
<meta property="og:url" content="<?=$SITE_CANONICAL_URL?>/privacy/">
<link rel="stylesheet" href="/css/common-dark.css">
<style>
label, label a { text-decoration: underline dotted white 1px; text-underline-offset: 0.2em; }

@ -16,7 +16,7 @@
<head>
<?php include "+components/page-head.php" ?>
<link rel="canonical" href="<?=$SITE_CANONICAL_URL?>/support/">
<link rel="stylesheet" href="/css/common-dark.css">
<meta name="description" content="<?php
?>Support sessioncommunities.online development with donations if you have disposable income. <?php
@ -26,7 +26,7 @@
?>sessioncommunities.online relies on donations for development and server costs. <?php
?>Help us redefine what it means to be sustainable and support our mission of improving the Session ecosystem!">
<meta property="og:type" content="article">
<meta property="og:url" content="<?=$SITE_CANONICAL_URL?>/support/">
<title>Support — sessioncommunities.online</title>
<style>
h1, h2, h3 {

Loading…
Cancel
Save