diff --git a/DysonNetwork.Sphere/Account/RelationshipService.cs b/DysonNetwork.Sphere/Account/RelationshipService.cs index 2d60c84..1e2b0d9 100644 --- a/DysonNetwork.Sphere/Account/RelationshipService.cs +++ b/DysonNetwork.Sphere/Account/RelationshipService.cs @@ -6,7 +6,7 @@ namespace DysonNetwork.Sphere.Account; public class RelationshipService(AppDatabase db, ICacheService cache) { - private const string UserFriendsCacheKeyPrefix = "user:friends:"; + private const string UserFriendsCacheKeyPrefix = "accounts:friends:"; public async Task HasExistingRelationship(Guid accountId, Guid relatedId) { diff --git a/DysonNetwork.Sphere/Activity/ActivityController.cs b/DysonNetwork.Sphere/Activity/ActivityController.cs index c82f27a..dcd4c34 100644 --- a/DysonNetwork.Sphere/Activity/ActivityController.cs +++ b/DysonNetwork.Sphere/Activity/ActivityController.cs @@ -22,8 +22,11 @@ public class ActivityController( /// Besides, when users are logged in, it will also mix the other kinds of data and who're plying to them. /// [HttpGet] - public async Task>> ListActivities([FromQuery] string? cursor, - [FromQuery] int take = 20) + public async Task>> ListActivities( + [FromQuery] string? cursor, + [FromQuery] string? filter, + [FromQuery] int take = 20 + ) { Instant? cursorTimestamp = null; if (!string.IsNullOrEmpty(cursor)) @@ -42,6 +45,6 @@ public class ActivityController( HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); return currentUserValue is not Account.Account currentUser ? Ok(await acts.GetActivitiesForAnyone(take, cursorTimestamp)) - : Ok(await acts.GetActivities(take, cursorTimestamp, currentUser)); + : Ok(await acts.GetActivities(take, cursorTimestamp, currentUser, filter)); } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Activity/ActivityService.cs b/DysonNetwork.Sphere/Activity/ActivityService.cs index e294a2f..5959ca3 100644 --- a/DysonNetwork.Sphere/Activity/ActivityService.cs +++ b/DysonNetwork.Sphere/Activity/ActivityService.cs @@ -42,26 +42,57 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS return activities; } - public async Task> GetActivities(int take, Instant? cursor, Account.Account currentUser) + public async Task> GetActivities( + int take, + Instant? cursor, + Account.Account currentUser, + string? filter = null + ) { var activities = new List(); var userFriends = await rels.ListAccountFriends(currentUser); var userPublishers = await pub.GetUserPublishers(currentUser.Id); - - var publishersId = userPublishers.Select(e => e.Id).ToList(); - - // Crunching data - var posts = await db.Posts + + // Get publishers based on filter + List? filteredPublishers = null; + switch (filter) + { + case "subscriptions": + filteredPublishers = await pub.GetSubscribedPublishers(currentUser.Id); + break; + case "friends": + { + filteredPublishers = (await pub.GetUserPublishersBatch(userFriends)) + .SelectMany(x => x.Value) + .DistinctBy(x => x.Id) + .ToList(); + break; + } + default: + break; + } + + var filteredPublishersId = filteredPublishers?.Select(e => e.Id).ToList(); + + // Build the query based on the filter + var postsQuery = db.Posts .Include(e => e.RepliedPost) .Include(e => e.ForwardedPost) .Include(e => e.Categories) .Include(e => e.Tags) - .Where(e => e.RepliedPostId == null || publishersId.Contains(e.RepliedPost!.PublisherId)) .Where(p => cursor == null || p.PublishedAt < cursor) .OrderByDescending(p => p.PublishedAt) - .FilterWithVisibility(currentUser, userFriends, userPublishers, isListing: true) + .AsQueryable(); + + if (filteredPublishersId is not null) + postsQuery = postsQuery.Where(p => filteredPublishersId.Contains(p.PublisherId)); + + // Complete the query with visibility filtering and execute + var posts = await postsQuery + .FilterWithVisibility(currentUser, userFriends, filter is null ? userPublishers : [], isListing: true) .Take(take) .ToListAsync(); + posts = await ps.LoadPostInfo(posts, currentUser, true); var postsId = posts.Select(e => e.Id).ToList(); diff --git a/DysonNetwork.Sphere/Chat/ChatService.cs b/DysonNetwork.Sphere/Chat/ChatService.cs index db0fcad..dc675aa 100644 --- a/DysonNetwork.Sphere/Chat/ChatService.cs +++ b/DysonNetwork.Sphere/Chat/ChatService.cs @@ -34,6 +34,7 @@ public partial class ChatService( using var scope = scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var webReader = scope.ServiceProvider.GetRequiredService(); + var newChat = scope.ServiceProvider.GetRequiredService(); // Preview the links in the message var updatedMessage = await PreviewMessageLinkAsync(message, webReader); @@ -62,7 +63,7 @@ public partial class ChatService( logger.LogDebug($"Updated message {message.Id} with {embedsList.Count} link previews"); // Notify clients of the updated message - await DeliverMessageAsync( + await newChat.DeliverMessageAsync( dbMessage, dbMessage.Sender, dbMessage.ChatRoom, diff --git a/DysonNetwork.Sphere/Post/PostService.cs b/DysonNetwork.Sphere/Post/PostService.cs index 5c7338a..2d0c210 100644 --- a/DysonNetwork.Sphere/Post/PostService.cs +++ b/DysonNetwork.Sphere/Post/PostService.cs @@ -659,6 +659,5 @@ public static class PostQueryExtensions .Where(e => e.Visibility != PostVisibility.Friends || (e.Publisher.AccountId != null && userFriends.Contains(e.Publisher.AccountId.Value)) || publishersId.Contains(e.PublisherId)); - ; } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Publisher/PublisherService.cs b/DysonNetwork.Sphere/Publisher/PublisherService.cs index 5de97df..c9e9182 100644 --- a/DysonNetwork.Sphere/Publisher/PublisherService.cs +++ b/DysonNetwork.Sphere/Publisher/PublisherService.cs @@ -9,6 +9,7 @@ namespace DysonNetwork.Sphere.Publisher; public class PublisherService(AppDatabase db, FileReferenceService fileRefService, ICacheService cache) { private const string UserPublishersCacheKey = "accounts:{0}:publishers"; + private const string UserPublishersBatchCacheKey = "accounts:batch:{0}:publishers"; public async Task> GetUserPublishers(Guid userId) { @@ -34,6 +35,86 @@ public class PublisherService(AppDatabase db, FileReferenceService fileRefServic return publishers; } + public async Task>> GetUserPublishersBatch(List userIds) + { + var result = new Dictionary>(); + var missingIds = new List(); + + // Try to get publishers from cache for each user + foreach (var userId in userIds) + { + var cacheKey = string.Format(UserPublishersCacheKey, userId); + var publishers = await cache.GetAsync>(cacheKey); + if (publishers != null) + result[userId] = publishers; + else + missingIds.Add(userId); + } + + if (missingIds.Count <= 0) return result; + { + // Fetch missing data from database + var publisherMembers = await db.PublisherMembers + .Where(p => missingIds.Contains(p.AccountId)) + .Select(p => new { p.AccountId, p.PublisherId }) + .ToListAsync(); + + var publisherIds = publisherMembers.Select(p => p.PublisherId).Distinct().ToList(); + var publishers = await db.Publishers + .Where(p => publisherIds.Contains(p.Id)) + .ToListAsync(); + + // Group publishers by user id + foreach (var userId in missingIds) + { + var userPublisherIds = publisherMembers + .Where(p => p.AccountId == userId) + .Select(p => p.PublisherId) + .ToList(); + + var userPublishers = publishers + .Where(p => userPublisherIds.Contains(p.Id)) + .ToList(); + + result[userId] = userPublishers; + + // Cache individual results + var cacheKey = string.Format(UserPublishersCacheKey, userId); + await cache.SetAsync(cacheKey, userPublishers, TimeSpan.FromMinutes(5)); + } + } + + return result; + } + + + + private const string SubscribedPublishersCacheKey = "accounts:{0}:subscribed-publishers"; + + public async Task> GetSubscribedPublishers(Guid userId) + { + var cacheKey = string.Format(SubscribedPublishersCacheKey, userId); + + // Try to get publishers from the cache first + var publishers = await cache.GetAsync>(cacheKey); + if (publishers is not null) + return publishers; + + // If not in cache, fetch from a database + var publishersId = await db.PublisherSubscriptions + .Where(p => p.AccountId == userId) + .Select(p => p.PublisherId) + .ToListAsync(); + publishers = await db.Publishers + .Where(p => publishersId.Contains(p.Id)) + .ToListAsync(); + + // Store in a cache for 5 minutes + await cache.SetAsync(cacheKey, publishers, TimeSpan.FromMinutes(5)); + + return publishers; + } + private const string PublisherMembersCacheKey = "publishers:{0}:members"; public async Task> GetPublisherMembers(Guid publisherId)