From da58e10d880ab61e42fe5184b1695507f961c22b Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 26 Jun 2025 19:17:28 +0800 Subject: [PATCH] :sparkles: Ranked posts --- .../Activity/ActivityService.cs | 70 ++++++++++++++++--- .../Activity/DiscoveryActivity.cs | 32 +++++++++ 2 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 DysonNetwork.Sphere/Activity/DiscoveryActivity.cs diff --git a/DysonNetwork.Sphere/Activity/ActivityService.cs b/DysonNetwork.Sphere/Activity/ActivityService.cs index d387ccf..d6942e6 100644 --- a/DysonNetwork.Sphere/Activity/ActivityService.cs +++ b/DysonNetwork.Sphere/Activity/ActivityService.cs @@ -1,19 +1,43 @@ + using DysonNetwork.Sphere.Account; +using DysonNetwork.Sphere.Discovery; using DysonNetwork.Sphere.Post; using DysonNetwork.Sphere.Publisher; using Microsoft.EntityFrameworkCore; using NodaTime; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace DysonNetwork.Sphere.Activity; -public class ActivityService(AppDatabase db, PublisherService pub, RelationshipService rels, PostService ps) +public class ActivityService(AppDatabase db, PublisherService pub, RelationshipService rels, PostService ps, DiscoveryService ds) { + private double CalculateHotRank(Post.Post post, Instant now) + { + var score = post.Upvotes - post.Downvotes; + var postTime = post.PublishedAt ?? post.CreatedAt; + var hours = (now - postTime).TotalHours; + // Add 1 to score to prevent negative results for posts with more downvotes than upvotes + return (score + 1) / Math.Pow(hours + 2, 1.8); + } + public async Task> GetActivitiesForAnyone(int take, Instant? cursor) { var activities = new List(); - // Crunching up data - var posts = await db.Posts + if (cursor == null) + { + var realms = await ds.GetPublicRealmsAsync(null, null); + if (realms.Count > 0) + { + activities.Add(new DiscoveryActivity("Explore Realms", realms.Cast().ToList()).ToActivity()); + } + } + + // Fetch a larger batch of recent posts to rank + var postsQuery = db.Posts .Include(e => e.RepliedPost) .Include(e => e.ForwardedPost) .Include(e => e.Categories) @@ -22,8 +46,9 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS .Where(p => cursor == null || p.PublishedAt < cursor) .OrderByDescending(p => p.PublishedAt) .FilterWithVisibility(null, [], [], isListing: true) - .Take(take) - .ToListAsync(); + .Take(take * 5); // Fetch more posts to have a good pool for ranking + + var posts = await postsQuery.ToListAsync(); posts = await ps.LoadPostInfo(posts, null, true); var postsId = posts.Select(e => e.Id).ToList(); @@ -32,8 +57,17 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS post.ReactionsCount = reactionMaps.TryGetValue(post.Id, out var count) ? count : new Dictionary(); + // Rank and sort + var now = SystemClock.Instance.GetCurrentInstant(); + var rankedPosts = posts + .Select(p => new { Post = p, Rank = CalculateHotRank(p, now) }) + .OrderByDescending(x => x.Rank) + .Select(x => x.Post) + .Take(take) + .ToList(); + // Formatting data - foreach (var post in posts) + foreach (var post in rankedPosts) activities.Add(post.ToActivity()); if (activities.Count == 0) @@ -53,6 +87,15 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS var userFriends = await rels.ListAccountFriends(currentUser); var userPublishers = await pub.GetUserPublishers(currentUser.Id); + if (cursor == null) + { + var realms = await ds.GetPublicRealmsAsync(null, null); + if (realms.Count > 0) + { + activities.Add(new DiscoveryActivity("Explore Realms", realms.Cast().ToList()).ToActivity()); + } + } + // Get publishers based on filter var filteredPublishers = filter switch { @@ -81,7 +124,7 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS // Complete the query with visibility filtering and execute var posts = await postsQuery .FilterWithVisibility(currentUser, userFriends, filter is null ? userPublishers : [], isListing: true) - .Take(take) + .Take(take * 5) // Fetch more posts to have a good pool for ranking .ToListAsync(); posts = await ps.LoadPostInfo(posts, currentUser, true); @@ -97,8 +140,17 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS await ps.IncreaseViewCount(post.Id, currentUser.Id.ToString()); } + // Rank and sort + var now = SystemClock.Instance.GetCurrentInstant(); + var rankedPosts = posts + .Select(p => new { Post = p, Rank = CalculateHotRank(p, now) }) + .OrderByDescending(x => x.Rank) + .Select(x => x.Post) + .Take(take) + .ToList(); + // Formatting data - foreach (var post in posts) + foreach (var post in rankedPosts) activities.Add(post.ToActivity()); if (activities.Count == 0) @@ -106,4 +158,4 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS return activities; } -} \ No newline at end of file +} diff --git a/DysonNetwork.Sphere/Activity/DiscoveryActivity.cs b/DysonNetwork.Sphere/Activity/DiscoveryActivity.cs new file mode 100644 index 0000000..4dd3119 --- /dev/null +++ b/DysonNetwork.Sphere/Activity/DiscoveryActivity.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using NodaTime; + +namespace DysonNetwork.Sphere.Activity +{ + public class DiscoveryActivity : IActivity + { + public string Title { get; set; } + public List Items { get; set; } + + public DiscoveryActivity(string title, List items) + { + Title = title; + Items = items; + } + + public Activity ToActivity() + { + var now = SystemClock.Instance.GetCurrentInstant(); + return new Activity + { + Id = Guid.NewGuid(), + Type = "discovery", + ResourceIdentifier = "discovery", + Data = this, + CreatedAt = now, + UpdatedAt = now, + }; + } + } +}