mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 19:27:55 +00:00
783 lines
27 KiB
JavaScript
783 lines
27 KiB
JavaScript
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const simplexDirectoryDataURL = 'https://directory.simplex.chat/data/';
|
|
|
|
// const simplexDirectoryDataURL = 'http://localhost:8080/directory-data/';
|
|
|
|
const simplexUsersGroup = 'SimpleX users group';
|
|
|
|
(function() {
|
|
const isMobile = {
|
|
Android: () => navigator.userAgent.match(/Android/i),
|
|
iOS: () => navigator.userAgent.match(/iPhone|iPad|iPod/i),
|
|
any: () => navigator.userAgent.match(/Android|iPhone|iPad|iPod/i)
|
|
};
|
|
|
|
function escapeHtml(text) {
|
|
return text
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'")
|
|
.replace(/\n/g, "<br>");
|
|
}
|
|
|
|
function escapeAttr(text) {
|
|
return text
|
|
.replace(/&/g, "&")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
}
|
|
|
|
const SAFE_URI_SCHEME = /^(https?:|simplex:|mailto:|tel:)/i;
|
|
|
|
function safeHref(uri) {
|
|
if (SAFE_URI_SCHEME.test(uri)) return escapeAttr(uri);
|
|
return escapeAttr(`javascript:void(alert('Potentially malicious link blocked:\\n'+${JSON.stringify(uri)}))`);
|
|
}
|
|
|
|
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" rel="noopener"'
|
|
}
|
|
|
|
const MAX_LIST_DEPTH = 7;
|
|
|
|
const MAX_TABLE_CELLS = 1000;
|
|
|
|
const COPY_BUTTON =
|
|
'<button type="button" class="md-copy" aria-label="Copy">' +
|
|
'<svg class="md-icon-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2.5"></rect><path d="M15 9V6.5A2.5 2.5 0 0 0 12.5 4h-6A2.5 2.5 0 0 0 4 6.5v6A2.5 2.5 0 0 0 6.5 15H9"></path></svg>' +
|
|
'<svg class="md-icon-done" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"></path></svg>' +
|
|
'</button>';
|
|
|
|
// navigator.clipboard is undefined outside secure contexts; fall back to execCommand
|
|
function copyToClipboard(text) {
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
return navigator.clipboard.writeText(text);
|
|
}
|
|
return new Promise(function(resolve, reject) {
|
|
const ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.style.position = 'fixed';
|
|
ta.style.top = '-9999px';
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
let ok = false;
|
|
try { ok = document.execCommand('copy'); } catch (e) {}
|
|
document.body.removeChild(ta);
|
|
ok ? resolve() : reject(new Error('copy failed'));
|
|
});
|
|
}
|
|
|
|
function handleCodeClick(e) {
|
|
const card = e.target.closest('.md-code');
|
|
if (!card) return;
|
|
const button = e.target.closest('.md-copy');
|
|
if (!button) {
|
|
card.classList.toggle('tapped');
|
|
return;
|
|
}
|
|
const pre = card.querySelector('pre');
|
|
if (!pre) return;
|
|
copyToClipboard(pre.innerText).then(() => {
|
|
button.classList.add('copied');
|
|
setTimeout(() => button.classList.remove('copied'), 1500);
|
|
}).catch(() => {});
|
|
}
|
|
|
|
function validBlocks(fts, blocks) {
|
|
let pos = 0;
|
|
const range = (r) => {
|
|
const valid = !!r && Number.isInteger(r.from) && Number.isInteger(r.to) && pos <= r.from && r.from <= r.to && r.to <= fts.length;
|
|
if (valid) pos = r.to;
|
|
return valid;
|
|
};
|
|
const entries = (items, depth) =>
|
|
Array.isArray(items) && (items.length === 0 || depth <= MAX_LIST_DEPTH) && items.every((e) => !!e && range(e.content) && entries(e.items, depth + 1));
|
|
const rows = (rs) => Array.isArray(rs) && rs.every((row) => Array.isArray(row) && row.every(range));
|
|
return Array.isArray(blocks) && blocks.every((b) => {
|
|
switch (b && b.type) {
|
|
case 'paragraph':
|
|
case 'heading':
|
|
case 'codeBlock':
|
|
case 'quote':
|
|
return range(b.content);
|
|
case 'list':
|
|
return entries(b.items, 1);
|
|
case 'table':
|
|
return Array.isArray(b.columns) && rows([b.header]) && rows(b.rows);
|
|
case 'rule':
|
|
return range(b.mark);
|
|
default:
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderBlocks(fts, blocks) {
|
|
let html = '';
|
|
let pos = 0;
|
|
const segments = (r) => renderSegments(fts.slice(r.from, r.to), true);
|
|
const isParagraphBreak = (ft) => ft.format && ft.format.type === 'markup' && ft.format.token && ft.format.token.type === 'paragraphBreak';
|
|
const gap = (to) => {
|
|
if (html && fts.slice(pos, to).some(isParagraphBreak)) html += '<div class="md-gap"></div>';
|
|
pos = Math.max(pos, to);
|
|
};
|
|
const block = (r, open, close) => {
|
|
gap(r.from);
|
|
html += open + segments(r) + close;
|
|
pos = r.to;
|
|
};
|
|
const entry = (e, level) => {
|
|
html += `<div class="md-entry" style="padding-left: ${1.25 * Math.min(level, 6)}em;"><span class="md-mark">${escapeHtml(String(e.number ?? '•'))}</span><span class="md-text">${segments(e.content)}</span></div>`;
|
|
pos = e.content.to;
|
|
e.items.forEach((item) => entry(item, level + 1));
|
|
};
|
|
const table = (b) => {
|
|
const rows = [b.header].concat(b.rows);
|
|
const width = b.columns.length * rows.length <= MAX_TABLE_CELLS ? b.columns.length : 0;
|
|
const align = (c) => (b.columns[c] === 'center' || b.columns[c] === 'right' ? b.columns[c] : 'left');
|
|
const cell = (tag, c, r) => `<${tag} style="text-align: ${align(c)};">${r ? segments(r) : ''}</${tag}>`;
|
|
const row = (cells, tag) =>
|
|
'<tr>' + cells.map((r, c) => cell(tag, c, r)).join('') + Array.from({ length: Math.max(0, width - cells.length) }, (_, k) => cell(tag, cells.length + k, null)).join('') + '</tr>';
|
|
gap(b.header.length > 0 ? b.header[0].from : pos);
|
|
html += '<div class="md-table"><div class="md-frame"><table>' + row(b.header, 'th') + b.rows.map((r) => row(r, 'td')).join('') + '</table></div></div>';
|
|
const last = rows[rows.length - 1];
|
|
pos = last.length > 0 ? last[last.length - 1].to : pos;
|
|
};
|
|
for (const b of blocks) {
|
|
switch (b.type) {
|
|
case 'paragraph':
|
|
block(b.content, '<div>', '</div>');
|
|
break;
|
|
case 'heading':
|
|
block(b.content, `<div class="md-h${b.level === 1 || b.level === 2 || b.level === 3 ? b.level : 4}">`, '</div>');
|
|
break;
|
|
case 'codeBlock':
|
|
block(b.content, '<div class="md-code"><pre>', '</pre>' + COPY_BUTTON + '</div>');
|
|
break;
|
|
case 'quote':
|
|
block(b.content, '<div class="md-quote"><span class="md-bar"></span><div class="md-text">', '</div></div>');
|
|
break;
|
|
case 'list':
|
|
if (b.items.length > 0) gap(b.items[0].content.from);
|
|
b.items.forEach((item) => entry(item, 0));
|
|
break;
|
|
case 'table':
|
|
table(b);
|
|
break;
|
|
case 'rule':
|
|
block(b.mark, '<hr class="md-rule">', '');
|
|
break;
|
|
}
|
|
}
|
|
return html;
|
|
}
|
|
|
|
function markupHtml(token) {
|
|
switch (token && token.type) {
|
|
case 'softBreak':
|
|
return ' ';
|
|
case 'lineBreak':
|
|
case 'hardBreak':
|
|
return '<br>';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function renderMarkdown(fts, blocks) {
|
|
return validBlocks(fts, blocks) ? renderBlocks(fts, blocks) : renderSegments(fts, false);
|
|
}
|
|
|
|
function renderSegments(fts, withBlocks) {
|
|
let html = '';
|
|
for (const ft of fts) {
|
|
const { format, text } = ft;
|
|
if (!format) {
|
|
html += escapeHtml(text);
|
|
continue;
|
|
}
|
|
try {
|
|
switch (format.type) {
|
|
case 'bold':
|
|
html += withBlocks && format.singleStar ? `<em>${escapeHtml(text)}</em>` : `<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 'small':
|
|
html += `<span class="small-text">${escapeHtml(text)}</span>`;
|
|
break;
|
|
case 'colored':
|
|
html += `<span class="${(format.color || '').replace(/[^a-zA-Z0-9_-]/g, '')}">${escapeHtml(text)}</span>`;
|
|
break;
|
|
case 'uri': {
|
|
let href = text.startsWith('http://') || text.startsWith('https://') || text.startsWith('simplex:/') ? text : 'https://' + text;
|
|
html += `<a href="${safeHref(href)}"${targetBlank(href)}>${escapeHtml(text)}</a>`;
|
|
break;
|
|
}
|
|
case 'hyperLink': {
|
|
const { showText, linkUri } = format;
|
|
html += `<a href="${safeHref(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="${safeHref(platformSimplexUri(simplexUri))}" target="_blank" rel="noopener">${linkText} <em>(${escapeHtml(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:${escapeAttr(text)}">${escapeHtml(text)}</a>`;
|
|
break;
|
|
case 'phone':
|
|
html += `<a href="tel:${escapeAttr(text)}">${escapeHtml(text)}</a>`;
|
|
break;
|
|
case 'escape':
|
|
break;
|
|
case 'markup':
|
|
html += withBlocks ? markupHtml(format.token) : escapeHtml(text);
|
|
break;
|
|
case 'unknown':
|
|
html += escapeHtml(text);
|
|
break;
|
|
default:
|
|
html += escapeHtml(text);
|
|
}
|
|
} catch(e) {
|
|
console.log(e);
|
|
html += escapeHtml(text);
|
|
}
|
|
}
|
|
return html;
|
|
}
|
|
|
|
const simplexAddressRegexp = /^simplex:\/([a-z]+)#(.+)/i;
|
|
|
|
const simplexShortLinkTypes = ["a", "c", "g", "i", "r"];
|
|
|
|
function platformSimplexUri(uri) {
|
|
if (isMobile.any()) return uri;
|
|
const res = uri.match(simplexAddressRegexp);
|
|
if (!res || !Array.isArray(res) || res.length < 3) return uri;
|
|
const linkType = res[1];
|
|
const fragment = res[2];
|
|
if (simplexShortLinkTypes.includes(linkType)) {
|
|
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}:/${linkType}#${newFragment}`;
|
|
} else {
|
|
return `https://simplex.chat/${linkType}#${fragment}`;
|
|
}
|
|
}
|
|
if (!document.location.pathname.startsWith('/directory')) return;
|
|
|
|
let allEntries = [];
|
|
|
|
let filteredEntries = [];
|
|
|
|
let currentSortMode = '';
|
|
|
|
let currentSearch = '';
|
|
|
|
let currentPage = 1;
|
|
|
|
async function initDirectory() {
|
|
const listing = await fetchJSON(simplexDirectoryDataURL + '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
|
|
|
|
applyHash();
|
|
|
|
searchInput.addEventListener('input', (e) => renderEntries('top', bySortPriority, topBtn, e.target.value.trim(), true));
|
|
liveBtn.addEventListener('click', () => renderEntries('live', byActiveAtDesc, liveBtn));
|
|
newBtn.addEventListener('click', () => renderEntries('new', byCreatedAtDesc, newBtn));
|
|
topBtn.addEventListener('click', () => renderEntries('top', bySortPriority, topBtn));
|
|
document.getElementById('directory').addEventListener('click', handleCodeClick);
|
|
window.addEventListener('popstate', applyHash);
|
|
|
|
function applyHash() {
|
|
const hash = location.hash;
|
|
let mode, comparator, btn, search = '';
|
|
switch (hash) {
|
|
case '#active':
|
|
mode = 'live';
|
|
comparator = byActiveAtDesc;
|
|
btn = liveBtn;
|
|
break;
|
|
case '#new':
|
|
mode = 'new';
|
|
comparator = byCreatedAtDesc;
|
|
btn = newBtn;
|
|
break;
|
|
default:
|
|
mode = 'top';
|
|
comparator = bySortPriority;
|
|
btn = topBtn;
|
|
try {
|
|
if (hash.startsWith('#q=')) {
|
|
search = decodeURIComponent(hash.slice(3));
|
|
if (search) searchInput.value = search;
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
currentSortMode = '';
|
|
currentSearch = '';
|
|
currentPage = 1;
|
|
renderEntries(mode, comparator, btn, search);
|
|
}
|
|
|
|
function renderEntries(mode, comparator, btn, search = '') {
|
|
if (currentSortMode === mode && search == currentSearch) return;
|
|
currentSortMode = mode;
|
|
const hash = search ? '#q=' + encodeURIComponent(search)
|
|
: mode === 'live' ? '#active'
|
|
: mode === 'new' ? '#new'
|
|
: '';
|
|
const url = hash || (location.pathname + location.search);
|
|
history.replaceState(null, '', url);
|
|
liveBtn.classList.remove('active');
|
|
newBtn.classList.remove('active');
|
|
topBtn.classList.remove('active');
|
|
if (search == '') {
|
|
currentSearch = '';
|
|
currentPage = 1;
|
|
searchInput.value = '';
|
|
btn.classList.add('active');
|
|
} else {
|
|
currentSearch = search;
|
|
}
|
|
filteredEntries = filterEntries(mode, search ?? '').sort(comparator);
|
|
renderDirectoryPage();
|
|
}
|
|
}
|
|
|
|
function renderDirectoryPage() {
|
|
const currentEntries = addPagination(filteredEntries);
|
|
displayEntries(currentEntries);
|
|
}
|
|
|
|
function filterEntries(mode, s) {
|
|
if (s === '' && mode == 'top') return allEntries.slice();
|
|
const query = s.toLowerCase();
|
|
return allEntries.filter(entry =>
|
|
( mode === 'top'
|
|
|| (mode === 'new' && entry.createdAt)
|
|
|| (mode === 'live' && entry.activeAt)
|
|
) &&
|
|
( query === ''
|
|
|| (entry.displayName || '').toLowerCase().includes(query)
|
|
|| includesQuery(entry.shortDescr, query)
|
|
|| includesQuery(entry.welcomeMessage, query)
|
|
|| simplexNameIncludesQuery(entry.simplexName, query)
|
|
)
|
|
);
|
|
}
|
|
|
|
function simplexNameIncludesQuery(name, query) {
|
|
if (!name) return false;
|
|
const q = normalizeSimplexName(query);
|
|
return q !== '' && normalizeSimplexName(name).includes(q);
|
|
}
|
|
|
|
function normalizeSimplexName(s) {
|
|
s = s.toLowerCase();
|
|
if (s.startsWith('#') || s.startsWith('@')) s = s.slice(1);
|
|
if (s.endsWith('.simplex')) s = s.slice(0, -'.simplex'.length);
|
|
return s;
|
|
}
|
|
|
|
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) {
|
|
return 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 status: ${response.status}`)
|
|
return await response.json()
|
|
} catch (e) {
|
|
console.error(e)
|
|
}
|
|
}
|
|
|
|
function bySortPriority(entry1, entry2) {
|
|
return entrySortPriority(entry2) - entrySortPriority(entry1);
|
|
}
|
|
|
|
function byActiveAtDesc(entry1, entry2) {
|
|
return (roundedTs(entry2.activeAt) - roundedTs(entry1.activeAt)) * 10
|
|
+ Math.sign(bySortPriority(entry1, entry2));
|
|
}
|
|
|
|
function byCreatedAtDesc(entry1, entry2) {
|
|
return (roundedTs(entry2.createdAt) - roundedTs(entry1.createdAt)) * 10
|
|
+ Math.sign(bySortPriority(entry1, entry2));
|
|
}
|
|
|
|
function roundedTs(s) {
|
|
try {
|
|
return new Date(s).valueOf();
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function entrySortPriority(entry) {
|
|
return entry.displayName === simplexUsersGroup
|
|
? Number.MAX_VALUE
|
|
: entryMemberCount(entry)
|
|
}
|
|
|
|
function entryMemberCount(entry) {
|
|
return entry.entryType.type == 'group'
|
|
? (entry.entryType.summary?.publicMemberCount ?? entry.entryType.summary?.currentMembers ?? 0)
|
|
: 0
|
|
}
|
|
|
|
const now = new Date();
|
|
const nowVal = now.valueOf();
|
|
const today = new Date(now);
|
|
today.setHours(0, 0, 0, 0);
|
|
const todayVal = today.valueOf();
|
|
const todayYear = today.getFullYear();
|
|
|
|
const dateFormatter = Intl?.DateTimeFormat?.(undefined, {month: '2-digit', day: '2-digit'});
|
|
const dateYearFormatter = Intl?.DateTimeFormat?.(undefined, {year: 'numeric', month: '2-digit', day: '2-digit'});
|
|
|
|
function showDate(d) {
|
|
return dateFormatter && d.getFullYear() == todayYear
|
|
? dateFormatter.format(d)
|
|
: dateYearFormatter?.format(d) ?? d.toLocaleDateString();
|
|
}
|
|
|
|
function showCreatedOn(s) {
|
|
const d = new Date(s)
|
|
d.setHours(0, 0, 0, 0);
|
|
return 'Created' + (d.valueOf() === todayVal ? ' today' : ' on ' + showDate(d));
|
|
}
|
|
|
|
function showActiveOn(s) {
|
|
const d = new Date(s)
|
|
const ago = nowVal - d.valueOf();
|
|
if (ago <= 1200000) return 'Active now'; // 20 minutes
|
|
if (ago <= 10800000) return 'Active recently'; // 3 hours
|
|
d.setHours(0, 0, 0, 0);
|
|
return 'Active' + (d.valueOf() === todayVal ? ' today' : ' on ' + showDate(d));
|
|
}
|
|
|
|
function displayEntries(entries) {
|
|
const directory = document.getElementById('directory');
|
|
directory.innerHTML = '';
|
|
|
|
for (let entry of entries) {
|
|
try {
|
|
const { entryType, displayName, simplexName, groupLink, shortDescr, shortDescrBlocks, welcomeMessage, welcomeMessageBlocks, 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-[#0B2A59] 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);
|
|
|
|
if (simplexName) {
|
|
const simplexNameElement = document.createElement('p');
|
|
simplexNameElement.textContent = simplexName;
|
|
simplexNameElement.className = 'text-sm font-medium';
|
|
textContainer.appendChild(simplexNameElement);
|
|
}
|
|
|
|
const welcomeMessageHTML = welcomeMessage ? renderMarkdown(welcomeMessage, welcomeMessageBlocks) : undefined;
|
|
const shortDescrHTML = shortDescr ? renderMarkdown(shortDescr, shortDescrBlocks) : undefined;
|
|
if (shortDescrHTML && welcomeMessageHTML?.includes(shortDescrHTML) !== true) {
|
|
const descrElement = document.createElement('p');
|
|
descrElement.innerHTML = shortDescrHTML;
|
|
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 = '▲';
|
|
} else {
|
|
messageElement.style.maxHeight = maxHeightPx;
|
|
readMore.className = 'read-more';
|
|
readMore.textContent = 'Read more';
|
|
}
|
|
});
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
if (entryType?.groupType) {
|
|
const noteElement = document.createElement('p');
|
|
noteElement.innerHTML = 'You need <a href="https://simplex.chat/downloads/">SimpleX Chat app v6.5</a> to join.';
|
|
noteElement.className = 'text-sm';
|
|
textContainer.appendChild(noteElement);
|
|
}
|
|
|
|
const entryTimestamp = currentSortMode === 'new' && entry.createdAt
|
|
? showCreatedOn(entry.createdAt)
|
|
: entry.activeAt
|
|
? showActiveOn(entry.activeAt)
|
|
: '';
|
|
if (entryTimestamp) {
|
|
timestampElement = document.createElement('p');
|
|
timestampElement.textContent = entryTimestamp;
|
|
timestampElement.className = 'text-sm';
|
|
textContainer.appendChild(timestampElement);
|
|
}
|
|
|
|
const memberCount = entryMemberCount(entry);
|
|
if (typeof memberCount == 'number' && memberCount > 0) {
|
|
const memberCountElement = document.createElement('p');
|
|
const isChannel = entryType?.groupType === 'channel';
|
|
memberCountElement.textContent = `${memberCount} ${isChannel ? 'subscribers' : 'members'}`;
|
|
memberCountElement.className = 'text-sm';
|
|
textContainer.appendChild(memberCountElement);
|
|
}
|
|
|
|
if (entryType?.admission?.review === "all") {
|
|
const knockingElement = document.createElement('p');
|
|
knockingElement.textContent = 'New members are reviewed by admins';
|
|
knockingElement.className = 'text-sm';
|
|
textContainer.appendChild(knockingElement);
|
|
}
|
|
|
|
const imgLinkElement = document.createElement('a');
|
|
imgLinkElement.className = 'img-link';
|
|
const groupLinkUri = groupLink.connShortLink ?? groupLink.connFullLink
|
|
try {
|
|
imgLinkElement.href = platformSimplexUri(groupLinkUri);
|
|
} catch(e) {
|
|
console.log(e);
|
|
imgLinkElement.href = groupLinkUri;
|
|
}
|
|
imgLinkElement.target = "_blank";
|
|
imgLinkElement.rel = entryType?.groupType === 'channel' ? "noopener simplex-channel" : "noopener simplex-group";
|
|
imgLinkElement.title = `Join ${displayName}`;
|
|
|
|
const imgElement = document.createElement('img');
|
|
imgElement.src = imageFile ? simplexDirectoryDataURL + imageFile : '/img/group.svg';
|
|
imgElement.alt = displayName;
|
|
imgElement.addEventListener('error', () => imgElement.src = '/img/group.svg');
|
|
imgLinkElement.appendChild(imgElement);
|
|
entryDiv.appendChild(imgLinkElement);
|
|
|
|
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) {
|
|
currentPage = p;
|
|
renderDirectoryPage();
|
|
}
|
|
|
|
function addPagination(entries) {
|
|
const entriesPerPage = 10;
|
|
const totalPages = Math.ceil(entries.length / entriesPerPage);
|
|
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 || (currentPage === 1 && p === 3) || (currentPage === totalPages && p === totalPages - 2)) {
|
|
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();
|
|
}
|
|
})();
|