diff --git a/apps/simplex-directory-service/src/Directory/Listing.hs b/apps/simplex-directory-service/src/Directory/Listing.hs index b9b50cb87b..cef478c273 100644 --- a/apps/simplex-directory-service/src/Directory/Listing.hs +++ b/apps/simplex-directory-service/src/Directory/Listing.hs @@ -23,13 +23,15 @@ import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64.URL as B64URL import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as LB +import Data.Int (Int64) import Data.List (isPrefixOf) import Data.Maybe (catMaybes, fromMaybe) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) -import Data.Time.Clock (UTCTime, getCurrentTime) +import Data.Time.Clock +import Data.Time.Clock.System import Data.Time.Format.ISO8601 (iso8601Show) import Directory.Store import Simplex.Chat.Markdown @@ -66,8 +68,8 @@ data DirectoryEntry = DirectoryEntry shortDescr :: Maybe MarkdownList, welcomeMessage :: Maybe MarkdownList, imageFile :: Maybe String, - activeAt :: UTCTime, - createdAt :: UTCTime + activeAt :: Maybe UTCTime, + createdAt :: Maybe UTCTime } $(JQ.deriveJSON defaultJSON ''DirectoryEntry) @@ -78,8 +80,18 @@ $(JQ.deriveJSON defaultJSON ''DirectoryListing) type ImageFileData = ByteString -groupDirectoryEntry :: GroupInfoSummary -> Maybe (DirectoryEntry, Maybe (FilePath, ImageFileData)) -groupDirectoryEntry (GIS GroupInfo {groupProfile, chatTs, createdAt} summary gLink_) = +newOrActive :: NominalDiffTime +newOrActive = 30 * nominalDay + +recentRoundedTime :: Int64 -> UTCTime -> UTCTime -> Maybe UTCTime +recentRoundedTime roundTo now t + | diffUTCTime now t > newOrActive = Nothing + | otherwise = + let secs = (systemSeconds (utcToSystemTime t) `div` roundTo) * roundTo + in Just $ systemToUTCTime $ MkSystemTime secs 0 + +groupDirectoryEntry :: UTCTime -> GroupInfoSummary -> Maybe (DirectoryEntry, Maybe (FilePath, ImageFileData)) +groupDirectoryEntry now (GIS GroupInfo {groupProfile, chatTs, createdAt} summary gLink_) = let GroupProfile {displayName, shortDescr, description, image, memberAdmission} = groupProfile entryType = DETGroup memberAdmission summary entry groupLink = @@ -91,8 +103,8 @@ groupDirectoryEntry (GIS GroupInfo {groupProfile, chatTs, createdAt} summary gLi shortDescr = toFormattedText <$> shortDescr, welcomeMessage = toFormattedText <$> description, imageFile = fst <$> imgData, - activeAt = fromMaybe createdAt chatTs, - createdAt + activeAt = recentRoundedTime 900 now $ fromMaybe createdAt chatTs, + createdAt = recentRoundedTime 86400 now createdAt } imgData = imgFileData groupLink =<< image in (de, imgData) @@ -121,7 +133,7 @@ generateListing st dir gs = do createDirectoryIfMissing True (newDir listingImageFolder) gs'' <- fmap catMaybes $ forM gs' $ \g@(GIS GroupInfo {groupId} _ _) -> - forM (groupDirectoryEntry g) $ \(g', img) -> do + forM (groupDirectoryEntry ts g) $ \(g', img) -> do forM_ img $ \(imgFile, imgData) -> B.writeFile (newDir imgFile) imgData pure (groupId, g') saveListing newDir listingFileName gs'' diff --git a/website/src/directory.html b/website/src/directory.html index 333d7faed8..1d87ffdaa9 100644 --- a/website/src/directory.html +++ b/website/src/directory.html @@ -263,7 +263,7 @@ templateEngineOverride: njk
diff --git a/website/src/js/directory.js b/website/src/js/directory.js index 313bdeedec..72c93d35fb 100644 --- a/website/src/js/directory.js +++ b/website/src/js/directory.js @@ -5,12 +5,14 @@ const directoryDataURL = 'https://directory.simplex.chat/data/'; let allEntries = []; -let sortedEntries = []; - let filteredEntries = []; let currentSortMode = ''; +let currentSearch = ''; + +let currentPage = 1; + async function initDirectory() { const listing = await fetchJSON(directoryDataURL + 'listing.json') const liveBtn = document.querySelector('#top-pagination .live'); @@ -18,24 +20,29 @@ async function initDirectory() { 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)); + renderEntries('top', bySortPriority, topBtn) + searchInput.addEventListener('input', (e) => renderEntries('top', bySortPriority, topBtn, e.target.value.trim())); + liveBtn.addEventListener('click', () => renderEntries('live', byActiveAtDesc, liveBtn, '')); + newBtn.addEventListener('click', () => renderEntries('new', byCreatedAtDesc, newBtn, '')); + topBtn.addEventListener('click', () => renderEntries('top', bySortPriority, topBtn, '')); - 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; + function renderEntries(mode, comparator, btn, search) { + if (currentSortMode === mode && search == currentSearch) 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); + if (search == '') { + currentSearch = ''; + currentPage = 1; + searchInput.value = ''; + btn.classList.add('active'); + } else { + currentSearch = search; + } + filteredEntries = filterEntries(mode, search ?? '').sort(comparator); + renderDirectoryPage(); } } @@ -44,18 +51,20 @@ function renderDirectoryPage() { 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 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) { @@ -91,18 +100,18 @@ async function fetchJSON(url) { } } -function byMemberCountDesc(entry1, entry2) { - return entryMemberCount(entry2) - entryMemberCount(entry1); +function bySortPriority(entry1, entry2) { + return entrySortPriority(entry2) - entrySortPriority(entry1); } function byActiveAtDesc(entry1, entry2) { return (roundedTs(entry2.activeAt) - roundedTs(entry1.activeAt)) * 10 - + Math.sign(byMemberCountDesc(entry1, entry2)); + + Math.sign(bySortPriority(entry1, entry2)); } function byCreatedAtDesc(entry1, entry2) { return (roundedTs(entry2.createdAt) - roundedTs(entry1.createdAt)) * 10 - + Math.sign(byMemberCountDesc(entry1, entry2)); + + Math.sign(bySortPriority(entry1, entry2)); } function roundedTs(s) { @@ -114,6 +123,14 @@ function roundedTs(s) { } } +const simplexUsersGroup = 'SimpleX users group'; + +function entrySortPriority(entry) { + return entry.displayName === simplexUsersGroup + ? Number.MAX_VALUE + : entryMemberCount(entry) +} + function entryMemberCount(entry) { return entry.entryType.type == 'group' ? (entry.entryType.summary?.currentMembers ?? 0) @@ -204,7 +221,7 @@ function displayEntries(entries) { console.log(e); imgElement.href = groupLinkUri; } - if (!isCurrentSite(imgElement.href)) imgElement.target = "_blank"; + imgElement.target = "_blank"; imgElement.title = `Join ${displayName}`; entryDiv.appendChild(imgElement); @@ -223,13 +240,13 @@ function displayEntries(entries) { } function goToPage(p) { - location.hash = p.toString(); + currentPage = p; + renderDirectoryPage(); } 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; @@ -376,7 +393,7 @@ function platformSimplexUri(uri) { if (remainingParams) newFragment += '?' + remainingParams; return `https://${host}:/${linkType}#${newFragment}`; } else { - return `https://simplex.chat/${fragment}`; + return `https://simplex.chat/${linkType}#${fragment}`; } }