diff --git a/.gitignore b/.gitignore
index 1cd2a09f40..24252a56fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,8 +54,9 @@ website/translations.json
website/src/img/images/
website/src/images/
website/src/js/lottie.min.js
-website/src/js/ethers*
+website/src/js/ethers.*
website/src/js/directory.js
+website/src/js/simplex-lib.js
website/src/file-assets/
website/src/link-images/
website/src/privacy.md
diff --git a/website/src/js/directory.js b/website/src/js/directory.js
deleted file mode 100644
index 64bb4b2097..0000000000
--- a/website/src/js/directory.js
+++ /dev/null
@@ -1,588 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-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(/\n/g, "
");
-}
-
-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 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 += `${escapeHtml(text)}`;
- break;
- case 'italic':
- html += `${escapeHtml(text)}`;
- break;
- case 'strikeThrough':
- html += `${escapeHtml(text)}`;
- break;
- case 'snippet':
- html += `${escapeHtml(text)}`;
- break;
- case 'secret':
- html += `${escapeHtml(text)}`;
- break;
- case 'small':
- html += `${escapeHtml(text)}`;
- break;
- case 'colored':
- html += `${escapeHtml(text)}`;
- break;
- case 'uri':
- let href = text.startsWith('http://') || text.startsWith('https://') || text.startsWith('simplex:/') ? text : 'https://' + text;
- html += `${escapeHtml(text)}`;
- break;
- case 'hyperLink': {
- const { showText, linkUri } = format;
- html += `${escapeHtml(showText ?? linkUri)}`;
- break;
- }
- case 'simplexLink': {
- const { showText, linkType, simplexUri, smpHosts } = format;
- const linkText = showText ? escapeHtml(showText) : getSimplexLinkDescr(linkType);
- html += `${linkText} (${viaHost(smpHosts)})`;
- break;
- }
- case 'command':
- html += `${escapeHtml(text)}`;
- break;
- case 'mention':
- html += `${escapeHtml(text)}`;
- break;
- case 'email':
- html += `${escapeHtml(text)}`;
- break;
- case 'phone':
- html += `${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}`;
- }
-}
-
-const simplexDirectoryDataURL = 'https://directory.simplex.chat/data/';
-
-// const simplexDirectoryDataURL = 'http://localhost:8080/directory-data/';
-
-const simplexUsersGroup = 'SimpleX users group';
-
-(function() {
-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));
- 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)
- )
- );
-}
-
-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, 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-[#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);
-
- 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 = '▲';
- } 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 SimpleX Chat app v6.5 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.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();
-}
-})();