✨ Filter on activities
This commit is contained in:
@ -6,7 +6,7 @@ namespace DysonNetwork.Sphere.Account;
|
|||||||
|
|
||||||
public class RelationshipService(AppDatabase db, ICacheService cache)
|
public class RelationshipService(AppDatabase db, ICacheService cache)
|
||||||
{
|
{
|
||||||
private const string UserFriendsCacheKeyPrefix = "user:friends:";
|
private const string UserFriendsCacheKeyPrefix = "accounts:friends:";
|
||||||
|
|
||||||
public async Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId)
|
public async Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId)
|
||||||
{
|
{
|
||||||
|
@ -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.
|
/// Besides, when users are logged in, it will also mix the other kinds of data and who're plying to them.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<List<Activity>>> ListActivities([FromQuery] string? cursor,
|
public async Task<ActionResult<List<Activity>>> ListActivities(
|
||||||
[FromQuery] int take = 20)
|
[FromQuery] string? cursor,
|
||||||
|
[FromQuery] string? filter,
|
||||||
|
[FromQuery] int take = 20
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Instant? cursorTimestamp = null;
|
Instant? cursorTimestamp = null;
|
||||||
if (!string.IsNullOrEmpty(cursor))
|
if (!string.IsNullOrEmpty(cursor))
|
||||||
@ -42,6 +45,6 @@ public class ActivityController(
|
|||||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||||
return currentUserValue is not Account.Account currentUser
|
return currentUserValue is not Account.Account currentUser
|
||||||
? Ok(await acts.GetActivitiesForAnyone(take, cursorTimestamp))
|
? Ok(await acts.GetActivitiesForAnyone(take, cursorTimestamp))
|
||||||
: Ok(await acts.GetActivities(take, cursorTimestamp, currentUser));
|
: Ok(await acts.GetActivities(take, cursorTimestamp, currentUser, filter));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -42,26 +42,57 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
|
|||||||
return activities;
|
return activities;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Activity>> GetActivities(int take, Instant? cursor, Account.Account currentUser)
|
public async Task<List<Activity>> GetActivities(
|
||||||
|
int take,
|
||||||
|
Instant? cursor,
|
||||||
|
Account.Account currentUser,
|
||||||
|
string? filter = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var activities = new List<Activity>();
|
var activities = new List<Activity>();
|
||||||
var userFriends = await rels.ListAccountFriends(currentUser);
|
var userFriends = await rels.ListAccountFriends(currentUser);
|
||||||
var userPublishers = await pub.GetUserPublishers(currentUser.Id);
|
var userPublishers = await pub.GetUserPublishers(currentUser.Id);
|
||||||
|
|
||||||
var publishersId = userPublishers.Select(e => e.Id).ToList();
|
// Get publishers based on filter
|
||||||
|
List<Publisher.Publisher>? 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;
|
||||||
|
}
|
||||||
|
|
||||||
// Crunching data
|
var filteredPublishersId = filteredPublishers?.Select(e => e.Id).ToList();
|
||||||
var posts = await db.Posts
|
|
||||||
|
// Build the query based on the filter
|
||||||
|
var postsQuery = db.Posts
|
||||||
.Include(e => e.RepliedPost)
|
.Include(e => e.RepliedPost)
|
||||||
.Include(e => e.ForwardedPost)
|
.Include(e => e.ForwardedPost)
|
||||||
.Include(e => e.Categories)
|
.Include(e => e.Categories)
|
||||||
.Include(e => e.Tags)
|
.Include(e => e.Tags)
|
||||||
.Where(e => e.RepliedPostId == null || publishersId.Contains(e.RepliedPost!.PublisherId))
|
|
||||||
.Where(p => cursor == null || p.PublishedAt < cursor)
|
.Where(p => cursor == null || p.PublishedAt < cursor)
|
||||||
.OrderByDescending(p => p.PublishedAt)
|
.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)
|
.Take(take)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
posts = await ps.LoadPostInfo(posts, currentUser, true);
|
posts = await ps.LoadPostInfo(posts, currentUser, true);
|
||||||
|
|
||||||
var postsId = posts.Select(e => e.Id).ToList();
|
var postsId = posts.Select(e => e.Id).ToList();
|
||||||
|
@ -34,6 +34,7 @@ public partial class ChatService(
|
|||||||
using var scope = scopeFactory.CreateScope();
|
using var scope = scopeFactory.CreateScope();
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
||||||
var webReader = scope.ServiceProvider.GetRequiredService<Connection.WebReader.WebReaderService>();
|
var webReader = scope.ServiceProvider.GetRequiredService<Connection.WebReader.WebReaderService>();
|
||||||
|
var newChat = scope.ServiceProvider.GetRequiredService<ChatService>();
|
||||||
|
|
||||||
// Preview the links in the message
|
// Preview the links in the message
|
||||||
var updatedMessage = await PreviewMessageLinkAsync(message, webReader);
|
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");
|
logger.LogDebug($"Updated message {message.Id} with {embedsList.Count} link previews");
|
||||||
|
|
||||||
// Notify clients of the updated message
|
// Notify clients of the updated message
|
||||||
await DeliverMessageAsync(
|
await newChat.DeliverMessageAsync(
|
||||||
dbMessage,
|
dbMessage,
|
||||||
dbMessage.Sender,
|
dbMessage.Sender,
|
||||||
dbMessage.ChatRoom,
|
dbMessage.ChatRoom,
|
||||||
|
@ -659,6 +659,5 @@ public static class PostQueryExtensions
|
|||||||
.Where(e => e.Visibility != PostVisibility.Friends ||
|
.Where(e => e.Visibility != PostVisibility.Friends ||
|
||||||
(e.Publisher.AccountId != null && userFriends.Contains(e.Publisher.AccountId.Value)) ||
|
(e.Publisher.AccountId != null && userFriends.Contains(e.Publisher.AccountId.Value)) ||
|
||||||
publishersId.Contains(e.PublisherId));
|
publishersId.Contains(e.PublisherId));
|
||||||
;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,6 +9,7 @@ namespace DysonNetwork.Sphere.Publisher;
|
|||||||
public class PublisherService(AppDatabase db, FileReferenceService fileRefService, ICacheService cache)
|
public class PublisherService(AppDatabase db, FileReferenceService fileRefService, ICacheService cache)
|
||||||
{
|
{
|
||||||
private const string UserPublishersCacheKey = "accounts:{0}:publishers";
|
private const string UserPublishersCacheKey = "accounts:{0}:publishers";
|
||||||
|
private const string UserPublishersBatchCacheKey = "accounts:batch:{0}:publishers";
|
||||||
|
|
||||||
public async Task<List<Publisher>> GetUserPublishers(Guid userId)
|
public async Task<List<Publisher>> GetUserPublishers(Guid userId)
|
||||||
{
|
{
|
||||||
@ -34,6 +35,86 @@ public class PublisherService(AppDatabase db, FileReferenceService fileRefServic
|
|||||||
return publishers;
|
return publishers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<Guid, List<Publisher>>> GetUserPublishersBatch(List<Guid> userIds)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<Guid, List<Publisher>>();
|
||||||
|
var missingIds = new List<Guid>();
|
||||||
|
|
||||||
|
// 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<List<Publisher>>(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<List<Publisher>> GetSubscribedPublishers(Guid userId)
|
||||||
|
{
|
||||||
|
var cacheKey = string.Format(SubscribedPublishersCacheKey, userId);
|
||||||
|
|
||||||
|
// Try to get publishers from the cache first
|
||||||
|
var publishers = await cache.GetAsync<List<Publisher>>(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";
|
private const string PublisherMembersCacheKey = "publishers:{0}:members";
|
||||||
|
|
||||||
public async Task<List<PublisherMember>> GetPublisherMembers(Guid publisherId)
|
public async Task<List<PublisherMember>> GetPublisherMembers(Guid publisherId)
|
||||||
|
Reference in New Issue
Block a user