website: directory page (#6283)

* website: directory page

* core: use markdown in directory entries

* render markdown on directory page

* update markdown

* toggle secrets on click

* update listings asynchronously

* add group links to the listing

* cleanup

* better directory layout with pagination

* script to run website

* update page navigation

* search

* readable markdown colors, better "read less"

* core: atomic update of directory listings, to avoid files unavailable

* fix symlink, sort entries on page with new first

* update listings every 15 min, add activeAt time

* fix sorting in the page and listing url

* replace simplex:/ links on desktop
This commit is contained in:
Evgeny
2025-09-20 19:47:50 +01:00
committed by GitHub
parent 429ec9d21a
commit a190d4ea9b
22 changed files with 994 additions and 191 deletions
+70 -58
View File
@@ -1,36 +1,42 @@
---
layout: layouts/main.html
title: "SimpleX blog: the latest news"
description: "SimpleX Chat - a private and encrypted messenger without any user IDs (not even random ones)! Make a private connection via link / QR code to send messages and make calls."
description: "SimpleX Chat - a private and encrypted messenger without any user IDs (not even random ones)! Make a
private connection via link / QR code to send messages and make calls."
path: /blog
templateEngineOverride: njk
active_blog: true
---
{% block css_links %}
<style>
#blog-list ul li {
list-style: disc;
margin-right: 0;
margin-left: 0;
}
#blog-list ul {
list-style-position: inside;
overflow: auto;
}
#blog-list ul li {
-webkit-margin-start: 1.1rem;
color: #000;
}
.dark #blog-list ul li {
color: #fff;
}
#blog-list ul li::marker {
color: black;
}
.dark #blog-list ul li::marker {
color: white;
}
</style>
<style>
#blog-list ul li {
list-style: disc;
margin-right: 0;
margin-left: 0;
}
#blog-list ul {
list-style-position: inside;
overflow: auto;
}
#blog-list ul li {
-webkit-margin-start: 1.1rem;
color: #000;
}
.dark #blog-list ul li {
color: #fff;
}
#blog-list ul li::marker {
color: black;
}
.dark #blog-list ul li::marker {
color: white;
}
</style>
{% endblock %}
<section class="py-10 px-5 mt-[66px]" id="blog-list">
@@ -39,42 +45,48 @@ active_blog: true
{% for blog in collections.blogs %}
{% if not(blog.data.draft) %}
<article class="w-full flex flex-col items-start md:flex-row rounded-[4px] overflow-hidden shadow-[0px_20px_30px_rgba(0,0,0,0.12)] dark:shadow-none bg-white dark:bg-[#11182F] mb-8">
<div class="min-h-[200px] h-[inherit] self-stretch md:w-[168px] bg-[#D9E7ED] dark:bg-[#17203D] flex items-center justify-center flex-[1] relative">
<div class="min-h-[inherit] h-full w-full flex items-end px-4 pt-4 justify-center relative">
{% if blog.data.image %}
{% if blog.data.imageBottom %}
<img class="w-full max-w-[240px] h-auto" src="{{ blog.data.image }}" alt="" srcset=""/>
{% elif blog.data.imageWide %}
<img class="mb-4 self-center w-full h-auto" src="{{ blog.data.image }}" alt="" srcset=""/>
{% else %}
<img class="mb-4 self-center w-full max-w-[240px] h-auto" src="{{ blog.data.image }}" alt="" srcset=""/>
{% endif %}
{% else %}
<img class="h-[44px] self-center dark:hidden" src="/img/new/logo-symbol-light.svg" alt="" srcset=""/>
<img class="h-[44px] self-center hidden dark:inline-block" src="/img/new/logo-symbol-dark.svg" alt="" srcset=""/>
{% endif %}
</div>
<article
class="w-full flex flex-col items-start md:flex-row rounded-[4px] overflow-hidden shadow-[0px_20px_30px_rgba(0,0,0,0.12)] dark:shadow-none bg-white dark:bg-[#11182F] mb-8">
<div
class="min-h-[200px] h-[inherit] self-stretch md:w-[168px] bg-[#D9E7ED] dark:bg-[#17203D] flex items-center justify-center flex-[1] relative">
<div class="min-h-[inherit] h-full w-full flex items-end px-4 pt-4 justify-center relative">
{% if blog.data.image %}
{% if blog.data.imageBottom %}
<img class="w-full max-w-[240px] h-auto" src="{{ blog.data.image }}" alt="" srcset="" />
{% elif blog.data.imageWide %}
<img class="mb-4 self-center w-full h-auto" src="{{ blog.data.image }}" alt="" srcset="" />
{% else %}
<img class="mb-4 self-center w-full max-w-[240px] h-auto" src="{{ blog.data.image }}" alt=""
srcset="" />
{% endif %}
{% else %}
<img class="h-[44px] self-center dark:hidden" src="/img/new/logo-symbol-light.svg" alt=""
srcset="" />
<img class="h-[44px] self-center hidden dark:inline-block" src="/img/new/logo-symbol-dark.svg"
alt="" srcset="" />
{% endif %}
</div>
<div class="p-6 md:py-8 flex-[2.5] flex flex-col">
<div>
<h1 class="text-grey-black dark:text-white !text-lg md:!text-xl font-bold ">
<a href="{{ blog.url }}">{{ blog.data.title | safe }}</a>
</h1>
<p class="text-sm text-[#A8B0B4] font-medium mt-2 mb-4 tracking-[0.03em]">
{{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }}
</p>
{% if blog.data.previewBody %}
<div class="mb-4 dark:text-white">
{% include blog.data.previewBody %}
</div>
{% elif blog.data.preview %}
<p class="dark:text-white mb-4">{{ blog.data.preview | safe }}</p>
{% endif %}
</div>
<div class="p-6 md:py-8 flex-[2.5] flex flex-col">
<div>
<h2 class="text-grey-black dark:text-white !text-lg md:!text-xl font-bold">
<a href="{{ blog.url }}">{{ blog.data.title | safe }}</a>
</h2>
<p class="text-sm text-[#A8B0B4] font-medium mt-2 mb-4 tracking-[0.03em]">
{{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }}
</p>
{% if blog.data.previewBody %}
<div class="mb-4 dark:text-white">
{% include blog.data.previewBody %}
</div>
<a class="block text-primary-light dark:text-[#70F0F9] text-base font-medium tracking-[0.03em] mt-auto" href="{{ blog.url }}">Read More</a>
{% elif blog.data.preview %}
<p class="dark:text-white mb-4">{{ blog.data.preview | safe }}</p>
{% endif %}
</div>
</article>
<a class="block text-primary-light dark:text-[#70F0F9] text-base font-medium tracking-[0.03em] mt-auto"
href="{{ blog.url }}">Read More</a>
</div>
</article>
{% endif %}
{% endfor %}
</div>
+272
View File
@@ -0,0 +1,272 @@
---
layout: layouts/main.html
title: "SimpleX Directory"
description: "Find communities on SimpleX network and create your own"
templateEngineOverride: njk
---
{% set lang = page.url | getlang %}
{% block js_scripts %}
<script src="/js/flag-anchor.js"></script>
<script async defer src="/js/directory.js"></script>
{% endblock %}
<style>
#directory .entry {
display: flex;
align-items: flex-start;
margin-bottom: 20px;
padding: 16px;
}
#directory .entry a {
order: -1;
object-fit: cover;
margin-right: 16px;
margin-bottom: 16px;
}
#directory .entry a img {
min-width: 104px;
min-height: 104px;
width: 104px;
height: 104px;
border-radius: 24px;
}
#directory .entry h2 {
margin: 0 0 5px 0;
}
#directory .entry p {
margin: 0 0 5px 0;
}
#directory .entry .secret {
filter: blur(5px);
cursor: pointer;
transition: filter 0.1s ease;
user-select: none;
}
#directory .entry .secret.visible {
filter: none;
user-select: auto;
}
#directory .entry .read-more {
color: #0053D0;
text-decoration: underline;
cursor: pointer;
}
#directory .entry .read-less {
color: darkgray;
cursor: pointer;
}
.dark #directory .entry .read-more {
color: #70F0F9;
}
#directory .entry .red {
color: #DD0000;
}
#directory .entry .green {
color: #20BD3D;
}
#directory .entry .blue {
color: #0053d0;
}
#directory .entry .cyan {
color: #0AC4D1;
}
#directory .entry .yellow {
color: #DEBD00;
}
#directory .entry .magenta {
color: magenta;
}
.dark #directory .entry .green {
color: #4DDA67;
}
.dark #directory .entry .blue {
color: #00A2FF;
}
.dark #directory .entry .cyan {
color: #70F0F9;
}
.dark #directory .entry .yellow {
color: #FFD700;
}
.dark #directory .entry .magenta {
color: magenta;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
margin-top: 20px;
padding: 10px 0;
}
.pagination button {
padding: 8px 12px;
border: none;
background-color: transparent;
color: #374151;
cursor: pointer;
border-radius: 50%;
font-size: 14px;
min-width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.dark .pagination button {
color: #70F0F9;
}
.pagination button:hover {
background-color: #f3f4f6;
}
.dark .pagination button:hover {
background-color: #1f2937;
}
.pagination button.active {
font-weight: bold;
color: #11182F;
}
.dark .pagination button.active {
color: #70F0F9;
}
.pagination button.text-btn {
border-radius: 20px;
min-width: auto;
height: 40px;
padding: 8px 16px;
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media (max-width: 768px) {
.pagination {
gap: 2px;
padding: 8px 0;
}
.pagination button {
font-size: 12px;
min-width: 32px;
height: 32px;
padding: 4px 8px;
}
.pagination button.text-btn {
padding: 4px 8px;
height: 32px;
border-radius: 16px;
font-size: 12px;
}
@media (max-width: 480px) {
.pagination button:not(.text-btn):not(.active):not(.neighbor) {
display: none;
}
.pagination button.active,
.pagination button.neighbor {
display: flex;
}
}
}
#search {
width: 100%;
max-width: 540px;
padding: 8px 12px 8px 32px;
font-size: 15px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
border: none;
border-radius: 10px;
background-color: #f2f2f7;
color: #000000;
outline: none;
transition: background-color 0.2s, box-shadow 0.2s;
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZT0iIzg4ODg4OCI+CjxjaXJjbGUgY3g9IjEwLjUiIGN5PSIxMC41IiByPSI3LjUiIC8+CjxsaW5lIHgxPSIxNiIgeTE9IjE2IiB4Mj0iMjEiIHkyPSIyMSIgLz4KPC9nPgo8L3N2Zz4=');
background-position: 8px center;
background-repeat: no-repeat;
background-size: 18px;
}
#search::placeholder {
color: #8e8e93;
}
.dark #search {
background-color: #1f2937;
color: #ffffff;
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZT0iI2JiYmJiYiI+CjxjaXJjbGUgY3g9IjEwLjUiIGN5PSIxMC41IiByPSI3LjUiIC8+CjxsaW5lIHgxPSIxNiIgeTE9IjE2IiB4Mj0iMjEiIHkyPSIyMSIgLz4KPC9nPgo8L3N2Zz4=');
}
.dark #search::placeholder {
color: #8e8e93;
}
.search-container {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
#search {
flex-grow: 1;
}
#top-pagination {
margin-bottom: 16px;
}
</style>
<section class="py-10 px-5 mt-[66px] dark:text-white">
<div class="container">
<h1 class="text-[38px] text-center font-bold text-active-blue mb-8">SimpleX Directory</h1>
<p>Welcome to the selected users' communities that you can join via <a href="/downloads">SimpleX Chat
app</a>.</p>
<p>SimpleX Directory is also available as a <a>SimpleX chat bot</a>.</p>
<p>Read about <a href="/docs/directory.html">how to add</a> your community</a>.</p>
<div class="search-container">
<input id="search">
<div id="top-pagination" class="pagination">
<button class="text-btn live">Active</button>
<button class="text-btn new">New</button>
<button class="text-btn top">Top</button>
</div>
</div>
<div id="directory" style="height: 3000px;"></div>
<div id="bottom-pagination" class="pagination"></div>
</div>
</section>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 326-->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 25.8008 25.459">
<g>
<rect height="25.459" opacity="0" width="25.8008" x="0" y="0" />
<path
d="M25.4395 12.7344C25.4395 19.7461 19.7266 25.459 12.7148 25.459C5.71289 25.459 0 19.7461 0 12.7344C0 5.73242 5.71289 0.0195312 12.7148 0.0195312C19.7266 0.0195312 25.4395 5.73242 25.4395 12.7344ZM4.21875 17.1094C4.21875 17.6855 4.53125 17.9785 5.27344 17.9785L10.1565 17.9785C10.0126 17.7153 9.96094 17.4251 9.96094 17.1582C9.96094 16.1237 10.5458 14.9671 11.5894 14.0584C10.8025 13.5615 9.83771 13.2715 8.84766 13.2715C6.47461 13.2715 4.21875 14.9512 4.21875 17.1094ZM10.8203 17.1582C10.8203 17.7148 11.1816 17.9785 12.0898 17.9785L20.1855 17.9785C21.1133 17.9785 21.4453 17.7148 21.4453 17.1582C21.4453 15.5371 19.4238 13.291 16.1621 13.291C12.8711 13.291 10.8203 15.5371 10.8203 17.1582ZM6.66016 9.87305C6.66016 11.2402 7.66602 12.2949 8.84766 12.2949C10.0488 12.2949 11.0449 11.2402 11.0449 9.86328C11.0449 8.53516 10.0293 7.5 8.84766 7.5C7.68555 7.5 6.66016 8.55469 6.66016 9.87305ZM13.6426 9.375C13.6426 10.9375 14.7754 12.1582 16.1621 12.1582C17.5195 12.1582 18.6621 10.9375 18.6621 9.36523C18.6621 7.83203 17.5098 6.65039 16.1621 6.65039C14.7852 6.65039 13.6426 7.86133 13.6426 9.375Z"
fill="#ccc" fill-opacity="0.85" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+439
View File
@@ -0,0 +1,439 @@
const directoryDataURL = 'https://directory.simplex.chat/data/';
// const directoryDataURL = 'http://localhost:8080/directory-data/';
let allEntries = [];
let sortedEntries = [];
let filteredEntries = [];
let currentSortMode = '';
async function initDirectory() {
const listing = await fetchJSON(directoryDataURL + 'listing.json')
const liveBtn = document.querySelector('#top-pagination .live');
const newBtn = document.querySelector('#top-pagination .new');
const topBtn = document.querySelector('#top-pagination .top');
const searchInput = document.getElementById('search');
allEntries = listing.entries
renderSortedEntries('top', byMemberCountDesc, topBtn)
window.addEventListener('hashchange', renderDirectoryPage);
searchInput.addEventListener('input', (e) => renderFilteredEntries(e.target.value));
liveBtn.addEventListener('click', () => renderSortedEntries('live', byActiveAtDesc, liveBtn));
newBtn.addEventListener('click', () => renderSortedEntries('new', byCreatedAtDesc, newBtn));
topBtn.addEventListener('click', () => renderSortedEntries('top', byMemberCountDesc, topBtn));
function renderSortedEntries(mode, comparator, btn) {
if (currentSortMode === mode) return;
currentSortMode = mode;
if (location.hash) location.hash = '';
liveBtn.classList.remove('active');
newBtn.classList.remove('active');
topBtn.classList.remove('active');
btn.classList.add('active');
sortedEntries = allEntries.slice().sort(comparator);
renderFilteredEntries(searchInput.value);
}
}
function renderDirectoryPage() {
const currentEntries = addPagination(filteredEntries);
displayEntries(currentEntries);
}
function renderFilteredEntries(s) {
const query = s.toLowerCase().trim();
if (query === '') {
filteredEntries = sortedEntries.slice();
} else {
filteredEntries = sortedEntries.filter(entry =>
(entry.displayName || '').toLowerCase().includes(query)
|| includesQuery(entry.shortDescr, query)
|| includesQuery(entry.welcomeMessage, query)
);
}
renderDirectoryPage();
}
function includesQuery(field, query) {
return field
&& Array.isArray(field)
&& field.some(ft => {
switch (ft.format?.type) {
case 'uri': return uriIncludesQuery(ft.text, query);
case 'hyperLink': return textIncludesQuery(ft.format.showText, query) || uriIncludesQuery(ft.format.linkUri, query);
case 'simplexLink': return textIncludesQuery(ft.format.showText, query);
default: return textIncludesQuery(ft.text, query);
}
});
}
function textIncludesQuery(text, query) {
text ? text.toLowerCase().includes(query) : false
}
function uriIncludesQuery(uri, query) {
if (!uri) return false;
uri = uri.toLowerCase();
return !uri.includes('simplex') && uri.includes(query);
}
async function fetchJSON(url) {
try {
const response = await fetch(url)
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`)
return await response.json()
} catch (error) {
console.error('Error fetching JSON:', error)
}
}
function byMemberCountDesc(entry1, entry2) {
return entryMemberCount(entry2) - entryMemberCount(entry1);
}
function byActiveAtDesc(entry1, entry2) {
return (roundedTs(entry2.activeAt) - roundedTs(entry1.activeAt)) * 10
+ Math.sign(byMemberCountDesc(entry1, entry2));
}
function byCreatedAtDesc(entry1, entry2) {
return (roundedTs(entry2.createdAt) - roundedTs(entry1.createdAt)) * 10
+ Math.sign(byMemberCountDesc(entry1, entry2));
}
function roundedTs(s) {
try {
// rounded to 15 minutes, which is the frequency of listing update
return Math.floor(new Date(s).valueOf() / 900000);
} catch {
return 0;
}
}
function entryMemberCount(entry) {
return entry.entryType.type == 'group'
? (entry.entryType.summary?.currentMembers ?? 0)
: 0
}
function displayEntries(entries) {
const directory = document.getElementById('directory');
directory.innerHTML = '';
for (let entry of entries) {
try {
const { entryType, displayName, groupLink, shortDescr, welcomeMessage, imageFile } = entry;
const entryDiv = document.createElement('div');
entryDiv.className = 'entry w-full flex flex-col items-start md:flex-row rounded-[4px] overflow-hidden shadow-[0px_20px_30px_rgba(0,0,0,0.12)] dark:shadow-none bg-white dark:bg-[#11182F] mb-8';
const textContainer = document.createElement('div');
textContainer.className = 'text-container';
const nameElement = document.createElement('h2');
nameElement.textContent = displayName;
nameElement.className = 'text-grey-black dark:text-white !text-lg md:!text-xl font-bold';
textContainer.appendChild(nameElement);
const welcomeMessageHTML = welcomeMessage ? renderMarkdown(welcomeMessage) : undefined;
const shortDescrHTML = shortDescr ? renderMarkdown(shortDescr) : undefined;
if (shortDescrHTML && welcomeMessageHTML?.includes(shortDescrHTML) !== true) {
const descrElement = document.createElement('p');
descrElement.innerHTML = renderMarkdown(shortDescr);
textContainer.appendChild(descrElement);
}
if (welcomeMessageHTML) {
const messageElement = document.createElement('p');
messageElement.innerHTML = welcomeMessageHTML;
textContainer.appendChild(messageElement);
const readMore = document.createElement('p');
readMore.textContent = 'Read more';
readMore.className = 'read-more';
readMore.style.display = 'none';
textContainer.appendChild(readMore);
setTimeout(() => {
const computedStyle = window.getComputedStyle(messageElement);
const lineHeight = parseFloat(computedStyle.lineHeight);
const maxLines = 5;
const maxHeight = maxLines * lineHeight
const maxHeightPx = `${maxHeight}px`;
messageElement.style.maxHeight = maxHeightPx;
messageElement.style.overflow = 'hidden';
if (messageElement.scrollHeight > maxHeight + 4) {
readMore.style.display = 'block';
readMore.addEventListener('click', () => {
if (messageElement.style.maxHeight === maxHeightPx) {
messageElement.style.maxHeight = 'none';
readMore.className = 'read-less';
readMore.innerHTML = '&#9650;';
} else {
messageElement.style.maxHeight = maxHeightPx;
readMore.className = 'read-more';
readMore.textContent = 'Read more';
}
});
}
}, 0);
}
const memberCount = entryMemberCount(entry);
if (typeof memberCount == 'number' && memberCount > 0) {
const memberCountElement = document.createElement('p');
memberCountElement.innerText = `${memberCount} members`;
memberCountElement.classList = ['text-sm'];
textContainer.appendChild(memberCountElement);
}
const imgElement = document.createElement('a');
imgSource =
imageFile
? directoryDataURL + imageFile
: "/img/group.svg";
imgElement.innerHTML = `<img src="${imgSource}" alt="${displayName}">`;
imgElement.href = platformSimplexUri(groupLink.connShortLink ?? groupLink.connFullLink);
if (!isCurrentSite(imgElement.href)) imgElement.target = "_blank";
imgElement.title = `Join ${displayName}`;
entryDiv.appendChild(imgElement);
entryDiv.appendChild(textContainer);
directory.appendChild(entryDiv);
} catch (e) {
console.log(e);
}
}
for (let el of document.querySelectorAll('.secret')) {
el.addEventListener('click', () => el.classList.toggle('visible'));
}
directory.style.height = '';
}
function goToPage(p) {
location.hash = p.toString();
}
function addPagination(entries) {
const entriesPerPage = 10;
const totalPages = Math.ceil(entries.length / entriesPerPage);
let currentPage = parseInt(location.hash.slice(1)) || 1;
if (currentPage < 1) currentPage = 1;
if (currentPage > totalPages) currentPage = totalPages;
const startIndex = (currentPage - 1) * entriesPerPage;
const endIndex = Math.min(startIndex + entriesPerPage, entries.length);
const currentEntries = entries.slice(startIndex, endIndex);
// addPaginationElements('top-pagination')
addPaginationElements('bottom-pagination')
return currentEntries;
function addPaginationElements(paginationId) {
const pagination = document.getElementById(paginationId);
if (!pagination) {
return currentEntries;
}
pagination.innerHTML = '';
try {
let startPage, endPage;
const pageButtonCount = 8
if (totalPages <= pageButtonCount) {
startPage = 1;
endPage = totalPages;
} else {
startPage = Math.max(1, currentPage - 4);
endPage = Math.min(totalPages, startPage + pageButtonCount - 1);
if (endPage - startPage + 1 < pageButtonCount) {
startPage = Math.max(1, endPage - pageButtonCount + 1);
}
}
// if (currentPage > 1 && startPage > 1) {
// const firstBtn = document.createElement('button');
// firstBtn.textContent = 'First';
// firstBtn.classList.add('text-btn');
// firstBtn.addEventListener('click', () => goToPage(1));
// pagination.appendChild(firstBtn);
// }
if (currentPage > 1) {
const prevBtn = document.createElement('button');
prevBtn.textContent = 'Prev';
prevBtn.classList.add('text-btn');
prevBtn.addEventListener('click', () => goToPage(currentPage - 1));
pagination.appendChild(prevBtn);
}
for (let p = startPage; p <= endPage; p++) {
const pageBtn = document.createElement('button');
pageBtn.textContent = p.toString();
if (p === currentPage) {
pageBtn.classList.add('active');
} else if (p === currentPage - 1 || p === currentPage + 1) {
pageBtn.classList.add('neighbor');
}
pageBtn.addEventListener('click', () => goToPage(p));
pagination.appendChild(pageBtn);
}
if (currentPage < totalPages) {
const nextBtn = document.createElement('button');
nextBtn.textContent = 'Next';
nextBtn.classList.add('text-btn');
nextBtn.addEventListener('click', () => goToPage(currentPage + 1));
pagination.appendChild(nextBtn);
}
// if (endPage < totalPages) {
// const lastBtn = document.createElement('button');
// lastBtn.textContent = 'Last';
// lastBtn.classList.add('text-btn');
// lastBtn.addEventListener('click', () => goToPage(totalPages));
// pagination.appendChild(lastBtn);
// }
} catch (e) {
console.log(e);
}
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDirectory);
} else {
initDirectory();
}
function escapeHtml(text) {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;")
.replace(/\n/g, "<br>");
}
function getSimplexLinkDescr(linkType) {
switch (linkType) {
case 'contact': return 'SimpleX contact address';
case 'invitation': return 'SimpleX one-time invitation';
case 'group': return 'SimpleX group link';
case 'channel': return 'SimpleX channel link';
case 'relay': return 'SimpleX relay link';
default: return 'SimpleX link';
}
}
function viaHost(smpHosts) {
const first = smpHosts[0] ?? '?';
return `via ${first}`;
}
function isCurrentSite(uri) {
return uri.startsWith("https://simplex.chat") || uri.startsWith("https://www.simplex.chat")
}
function targetBlank(uri) {
return isCurrentSite(uri) ? '' : ' target="_blank"'
}
function platformSimplexUri(uri) {
if (isMobile.any()) return uri;
if (uri.startsWith('simplex:/g#')) {
const prefixLength = 'simplex:/g#'.length;
const fragment = uri.substring(prefixLength);
const queryIndex = fragment.indexOf('?');
if (queryIndex === -1) return uri;
const hashPart = fragment.substring(0, queryIndex);
const queryStr = fragment.substring(queryIndex + 1);
const params = new URLSearchParams(queryStr);
const host = params.get('h');
if (!host) return uri;
params.delete('h');
let newFragment = hashPart;
const remainingParams = params.toString();
if (remainingParams) newFragment += '?' + remainingParams;
return `https://${host}:/g#${newFragment}`;
} else if(uri.startsWith('simplex:/')) {
const prefixLength = 'simplex:/'.length;
return 'https://simplex.chat/' + uri.substring(prefixLength);
} else {
return uri;
}
}
function renderMarkdown(fts) {
let html = '';
for (const ft of fts) {
const { format, text } = ft;
if (!format) {
html += escapeHtml(text);
continue;
}
try {
switch (format.type) {
case 'bold':
html += `<strong>${escapeHtml(text)}</strong>`;
break;
case 'italic':
html += `<em>${escapeHtml(text)}</em>`;
break;
case 'strikeThrough':
html += `<s>${escapeHtml(text)}</s>`;
break;
case 'snippet':
html += `<span style="font-family: monospace;">${escapeHtml(text)}</span>`;
break;
case 'secret':
html += `<span class="secret">${escapeHtml(text)}</span>`;
break;
case 'colored':
html += `<span class="${format.color}">${escapeHtml(text)}</span>`;
break;
case 'uri':
let href = text.startsWith('http://') || text.startsWith('https://') || text.startsWith('simplex:/') ? text : 'https://' + text;
html += `<a href="${href}"${targetBlank(href)}>${escapeHtml(text)}</a>`;
break;
case 'hyperLink': {
const { showText, linkUri } = format;
html += `<a href="${linkUri}"${targetBlank(linkUri)}>${escapeHtml(showText ?? linkUri)}</a>`;
break;
}
case 'simplexLink': {
const { showText, linkType, simplexUri, smpHosts } = format;
const linkText = showText ? escapeHtml(showText) : getSimplexLinkDescr(linkType);
html += `<a href="${platformSimplexUri(simplexUri)}">${linkText} <em>(${viaHost(smpHosts)})</em></a>`;
break;
}
case 'command':
html += `<span style="font-family: monospace;">${escapeHtml(text)}</span>`;
break;
case 'mention':
html += `<strong>${escapeHtml(text)}</strong>`;
break;
case 'email':
html += `<a href="mailto:${text}">${escapeHtml(text)}</a>`;
break;
case 'phone':
html += `<a href="tel:${text}">${escapeHtml(text)}</a>`;
break;
case 'unknown':
html += escapeHtml(text);
break;
default:
html += escapeHtml(text);
}
} catch {
html += escapeHtml(text);
}
}
return html;
}
+2 -1
View File
@@ -26,7 +26,8 @@ const uniqueSwiper = new Swiper('.unique-swiper', {
const isMobile = {
Android: () => navigator.userAgent.match(/Android/i),
iOS: () => navigator.userAgent.match(/iPhone|iPad|iPod/i)
iOS: () => navigator.userAgent.match(/iPhone|iPad|iPod/i),
any: () => navigator.userAgent.match(/Android|iPhone|iPad|iPod/i)
};
const privateSwiper = new Swiper('.private-swiper', {