diff --git a/DysonNetwork.Sphere/AppDatabase.cs b/DysonNetwork.Sphere/AppDatabase.cs index 9ba856e..3393ab7 100644 --- a/DysonNetwork.Sphere/AppDatabase.cs +++ b/DysonNetwork.Sphere/AppDatabase.cs @@ -80,6 +80,7 @@ public class AppDatabase( Nodes = { PermissionService.NewPermissionNode("group:default", "global", "posts.create", true), + PermissionService.NewPermissionNode("group:default", "global", "posts.reactions.create", true), PermissionService.NewPermissionNode("group:default", "global", "publishers.create", true), PermissionService.NewPermissionNode("group:default", "global", "files.create", true), PermissionService.NewPermissionNode("group:default", "global", "chat.create", true) diff --git a/DysonNetwork.Sphere/Post/Post.cs b/DysonNetwork.Sphere/Post/Post.cs index c6a11fd..7411367 100644 --- a/DysonNetwork.Sphere/Post/Post.cs +++ b/DysonNetwork.Sphere/Post/Post.cs @@ -42,6 +42,7 @@ public class Post : ModelBase public int ViewsTotal { get; set; } public int Upvotes { get; set; } public int Downvotes { get; set; } + [NotMapped] public Dictionary ReactionsCount { get; set; } = new(); public long? ThreadedPostId { get; set; } public Post? ThreadedPost { get; set; } @@ -106,5 +107,6 @@ public class PostReaction : ModelBase public long PostId { get; set; } [JsonIgnore] public Post Post { get; set; } = null!; + public long AccountId { get; set; } public Account.Account Account { get; set; } = null!; -} \ No newline at end of file +} diff --git a/DysonNetwork.Sphere/Post/PostController.cs b/DysonNetwork.Sphere/Post/PostController.cs index 59e85b0..e358822 100644 --- a/DysonNetwork.Sphere/Post/PostController.cs +++ b/DysonNetwork.Sphere/Post/PostController.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json; using DysonNetwork.Sphere.Account; using DysonNetwork.Sphere.Permission; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NodaTime; @@ -36,6 +37,11 @@ public class PostController(AppDatabase db, PostService ps, RelationshipService .Take(take) .ToListAsync(); posts = PostService.TruncatePostContent(posts); + + var postsId = posts.Select(e => e.Id).ToList(); + var reactionMaps = await ps.GetPostReactionMapBatch(postsId); + foreach (var post in posts) + post.ReactionsCount = reactionMaps[post.Id]; Response.Headers["X-Total"] = totalCount.ToString(); @@ -62,6 +68,8 @@ public class PostController(AppDatabase db, PostService ps, RelationshipService .FirstOrDefaultAsync(); if (post is null) return NotFound(); + post.ReactionsCount = await ps.GetPostReactionMap(post.Id); + return Ok(post); } @@ -73,13 +81,13 @@ public class PostController(AppDatabase db, PostService ps, RelationshipService var currentUser = currentUserValue as Account.Account; var userFriends = currentUser is null ? [] : await rels.ListAccountFriends(currentUser); - var post = await db.Posts + var parent = await db.Posts .Where(e => e.Id == id) .FirstOrDefaultAsync(); - if (post is null) return NotFound(); + if (parent is null) return NotFound(); var totalCount = await db.Posts - .Where(e => e.RepliedPostId == post.Id) + .Where(e => e.RepliedPostId == parent.Id) .FilterWithVisibility(currentUser, userFriends, isListing: true) .CountAsync(); var posts = await db.Posts @@ -96,6 +104,11 @@ public class PostController(AppDatabase db, PostService ps, RelationshipService .Take(take) .ToListAsync(); posts = PostService.TruncatePostContent(posts); + + var postsId = posts.Select(e => e.Id).ToList(); + var reactionMaps = await ps.GetPostReactionMapBatch(postsId); + foreach (var post in posts) + post.ReactionsCount = reactionMaps[post.Id]; Response.Headers["X-Total"] = totalCount.ToString(); @@ -194,6 +207,45 @@ public class PostController(AppDatabase db, PostService ps, RelationshipService return post; } + public class PostReactionRequest + { + [MaxLength(256)] public string Symbol { get; set; } = null!; + public PostReactionAttitude Attitude { get; set; } + } + + [HttpPost("{id:long}/reactions")] + [Authorize] + [RequiredPermission("global", "posts.reactions.create")] + public async Task> CreatePostReaction(long id, [FromBody] PostReactionRequest request) + { + HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); + if (currentUserValue is not Account.Account currentUser) return Unauthorized(); + var userFriends = await rels.ListAccountFriends(currentUser); + + var post = await db.Posts + .Where(e => e.Id == id) + .Include(e => e.Publisher) + .FilterWithVisibility(currentUser, userFriends) + .FirstOrDefaultAsync(); + if (post is null) return NotFound(); + + var isExistingReaction = await db.PostReactions + .AnyAsync(r => r.PostId == post.Id && + r.Symbol == request.Symbol && + r.AccountId == currentUser.Id); + var reaction = new PostReaction + { + Symbol = request.Symbol, + Attitude = request.Attitude, + PostId = post.Id, + AccountId = currentUser.Id + }; + await ps.ModifyPostVotes(post, reaction, isExistingReaction); + + if (isExistingReaction) return NoContent(); + return Ok(reaction); + } + [HttpPatch("{id:long}")] public async Task> UpdatePost(long id, [FromBody] PostRequest request) { diff --git a/DysonNetwork.Sphere/Post/PostService.cs b/DysonNetwork.Sphere/Post/PostService.cs index 6cfc0ec..778c82d 100644 --- a/DysonNetwork.Sphere/Post/PostService.cs +++ b/DysonNetwork.Sphere/Post/PostService.cs @@ -125,12 +125,12 @@ public class PostService(AppDatabase db, FileService fs, ActivityService act) if (post.Content?.RootElement is { ValueKind: JsonValueKind.Array }) { var searchTextBuilder = new System.Text.StringBuilder(); - + if (!string.IsNullOrWhiteSpace(post.Title)) searchTextBuilder.AppendLine(post.Title); if (!string.IsNullOrWhiteSpace(post.Description)) searchTextBuilder.AppendLine(post.Description); - + foreach (var element in post.Content.RootElement.EnumerateArray()) { if (element is { ValueKind: JsonValueKind.Object } && @@ -140,6 +140,7 @@ public class PostService(AppDatabase db, FileService fs, ActivityService act) searchTextBuilder.Append(insertProperty.GetString()); } } + post.SearchVector = EF.Functions.ToTsVector(searchTextBuilder.ToString().Trim()); } @@ -225,12 +226,12 @@ public class PostService(AppDatabase db, FileService fs, ActivityService act) if (post.Content?.RootElement is { ValueKind: JsonValueKind.Array }) { var searchTextBuilder = new System.Text.StringBuilder(); - + if (!string.IsNullOrWhiteSpace(post.Title)) searchTextBuilder.AppendLine(post.Title); if (!string.IsNullOrWhiteSpace(post.Description)) searchTextBuilder.AppendLine(post.Description); - + foreach (var element in post.Content.RootElement.EnumerateArray()) { if (element is { ValueKind: JsonValueKind.Object } && @@ -240,6 +241,7 @@ public class PostService(AppDatabase db, FileService fs, ActivityService act) searchTextBuilder.Append(insertProperty.GetString()); } } + post.SearchVector = EF.Functions.ToTsVector(searchTextBuilder.ToString().Trim()); } @@ -255,6 +257,62 @@ public class PostService(AppDatabase db, FileService fs, ActivityService act) await db.SaveChangesAsync(); await fs.MarkUsageRangeAsync(post.Attachments, -1); } + + /// + /// Calculate the total number of votes for a post. + /// This function helps you save the new reactions. + /// + /// Post that modifying + /// The new / target reaction adding / removing + /// Indicate this operation is adding / removing + public async Task ModifyPostVotes(Post post, PostReaction reaction, bool isRemoving) + { + var isExistingReaction = await db.Set() + .AnyAsync(r => r.PostId == post.Id && r.AccountId == reaction.AccountId); + if (isExistingReaction) return; + + if (!isRemoving) + { + db.Add(reaction); + switch (reaction.Attitude) + { + case PostReactionAttitude.Positive: + post.Upvotes++; + break; + case PostReactionAttitude.Negative: + post.Downvotes++; + break; + } + } + + await db.SaveChangesAsync(); + } + + public async Task> GetPostReactionMap(long postId) + { + return await db.Set() + .Where(r => r.PostId == postId) + .GroupBy(r => r.Symbol) + .ToDictionaryAsync( + g => g.Key, + g => g.Count() + ); + } + + public async Task>> GetPostReactionMapBatch(List postIds) + { + return await db.Set() + .Where(r => postIds.Contains(r.PostId)) + .GroupBy(r => r.PostId) + .ToDictionaryAsync( + g => g.Key, + g => g.GroupBy(r => r.Symbol) + .ToDictionary( + sg => sg.Key, + sg => sg.Count() + ) + ); + } } public static class PostQueryExtensions diff --git a/DysonNetwork.sln.DotSettings.user b/DysonNetwork.sln.DotSettings.user index 863727e..10180ba 100644 --- a/DysonNetwork.sln.DotSettings.user +++ b/DysonNetwork.sln.DotSettings.user @@ -6,6 +6,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded