directory: show only recent groups in active/new tabs, page improvements (#6290)

* directory: show only recent groups as active or new

* round times

* sorting order

* fix links

* improve
This commit is contained in:
Evgeny
2025-09-21 14:36:58 +01:00
committed by GitHub
parent 83b655ea83
commit c09665b920
3 changed files with 72 additions and 43 deletions

View File

@@ -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''

View File

@@ -263,7 +263,7 @@ templateEngineOverride: njk
<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>
<button class="text-btn top">All</button>
</div>
</div>
<div id="directory" style="height: 3000px;"></div>

View File

@@ -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}`;
}
}