(search) Add clustering to subscriptions view

This commit is contained in:
Viktor Lofgren 2024-12-18 15:12:22 +01:00
parent 2a3c63f209
commit 6d18e6d840
2 changed files with 75 additions and 24 deletions

View File

@ -13,11 +13,9 @@ import nu.marginalia.search.model.NavbarModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/** Renders the front page (index) */
@Singleton
@ -46,7 +44,7 @@ public class SearchFrontPageService {
@Path("/")
public MapModelAndView render(Context context) {
List<NewsItem> newsItems = getNewsItems(context);
List<NewsItemCluster> newsItems = getNewsItems(context);
IndexModel model = new IndexModel(newsItems, searchVisitorCount.getQueriesPerMinute());
@ -56,7 +54,7 @@ public class SearchFrontPageService {
.put("websiteUrl", websiteUrl);
}
private List<NewsItem> getNewsItems(Context context) {
private List<NewsItemCluster> getNewsItems(Context context) {
Set<Integer> subscriptions = subscriptionService.getSubscriptions(context);
@ -69,7 +67,8 @@ public class SearchFrontPageService {
feedResults.add(feedsClient.getFeed(sub));
}
List<NewsItem> ret = new ArrayList<>();
List<NewsItem> itemsAll = new ArrayList<>();
for (var result : feedResults) {
try {
RpcFeed feed = result.get();
@ -79,7 +78,10 @@ public class SearchFrontPageService {
if (title.isBlank()) {
title = "[Missing Title]";
}
ret.add(new NewsItem(title, item.getUrl(), feed.getDomain(), item.getDescription(), item.getDate()));
itemsAll.add(
new NewsItem(title, item.getUrl(), feed.getDomain(), item.getDescription(), item.getDate())
);
}
}
catch (Exception ex) {
@ -87,11 +89,29 @@ public class SearchFrontPageService {
}
}
ret.sort(Comparator.comparing(NewsItem::date).reversed());
if (ret.size() > 25) {
ret.subList(25, ret.size()).clear();
Map<String, List<NewsItem>> ret =
itemsAll.stream()
.sorted(Comparator.comparing(NewsItem::date).reversed())
.collect(Collectors.groupingBy(NewsItem::domain));
List<NewsItemCluster> items = new ArrayList<>(ret.size());
for (var itemsForDomain : ret.values()) {
itemsForDomain.sort(
Comparator
.comparing(NewsItem::date)
.reversed()
);
items.add(new NewsItemCluster(itemsForDomain));
}
return ret;
items.sort(Comparator.comparing((NewsItemCluster item) -> item.first().date).reversed());
// No more than 20 news item clusters on the front page
if (items.size() > 20) {
items.subList(20, items.size()).clear();
}
return items;
}
@ -134,6 +154,19 @@ public class SearchFrontPageService {
return sb.toString();
}*/
public record IndexModel(List<NewsItem> news, int searchPerMinute) { }
public record NewsItem(String title, String url, String domain, String description, String date) {}
public record IndexModel(List<NewsItemCluster> news, int searchPerMinute) { }
public record NewsItem(String title,
String url,
String domain,
String description,
String date
) {}
public record NewsItemCluster(
NewsItem first,
List<NewsItem> rest) {
public NewsItemCluster(List<NewsItem> items) {
this(items.getFirst(), items.subList(1, Math.min(5, items.size())));
}
}
}

View File

@ -4,6 +4,7 @@
@import nu.marginalia.search.model.SearchProfile
@import nu.marginalia.search.svc.SearchFrontPageService.IndexModel
@import nu.marginalia.search.svc.SearchFrontPageService.NewsItem
@import nu.marginalia.search.svc.SearchFrontPageService.NewsItemCluster
@param NavbarModel navbar
@param WebsiteUrl websiteUrl
@ -81,21 +82,38 @@
@else
<div class="max-w-7xl mx-auto flex flex-col space-y-4 fill-w m-4">
<div class="my-4 text-black dark:text-white font-serif text-xl mx-8">Subscriptions</div>
@for (NewsItem item : model.news())
<div class="border dark:border-gray-600 rounded bg-white dark:bg-gray-800 flex flex-col overflow-hidden mx-4 p-4 space-y-2">
<div class="text-black dark:text-white flex place-items-middle">
<div class="flex flex-col flex-col space-y-1">
<a class="text-l text-liteblue dark:text-blue-200" href="${item.url()}" rel="ugc external nofollow">${item.title()}</a>
<a class="text-xs text-gray-800 dark:text-gray-100" href="/site/${item.domain()}">
<i class="fas fa-globe"></i> ${item.domain()}
</a>
@for (NewsItemCluster cluster : model.news())
!{NewsItem item = cluster.first();}
<div class="border dark:border-gray-600 rounded bg-white dark:bg-gray-800 flex flex-col overflow-hidden mx-4 space-y-2">
<a class="flex space-x-2 flex-row place-items-baseline bg-margeblue text-white p-2 text-md" href="/site/${item.domain()}">
<i class="fas fa-globe mr-2"></i>
<span>${item.domain()}</span>
</a>
<div class="text-black dark:text-white flex place-items-middle mx-2">
<div class="flex flex-col space-y-1">
<a class="text-l text-liteblue dark:text-blue-200 visited:text-purple-800 dark:visited:text-purple-200" href="${item.url()}" rel="ugc external nofollow">${item.title()}</a>
</div>
<div class="grow"></div>
<div class="flex text-xs">
<div class="flex text-xs whitespace-nowrap">
${item.date().substring(0, 10)}
</div>
</div>
<div class="text-sm text-gray-800 dark:text-gray-100">$unsafe{item.description()}</div>
<div class="text-sm text-gray-800 dark:text-gray-100 mx-2">$unsafe{item.description()}</div>
@for (var remainder : cluster.rest())
<div class="flex flex-col space-y-1 dark:border-gray-600 border-t pt-2 mx-2">
<div class="text-black dark:text-white flex place-items-middle">
<a class="text-sm text-liteblue dark:text-blue-200 visited:text-purple-800 dark:visited:text-purple-200" href="${remainder.url()}" rel="ugc external nofollow">${remainder.title()}</a>
<div class="grow"></div>
<div class="flex text-xs whitespace-nowrap">
${remainder.date().substring(0, 10)}
</div>
</div>
</div>
@endfor
<div class="pt-2"></div>
</div>
@endfor
</div>