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(); -} -})();