diff --git a/Femto.Api/Controllers/Posts/Dto/AddPostReactionRequest.cs b/Femto.Api/Controllers/Posts/Dto/AddPostReactionRequest.cs new file mode 100644 index 0000000..36330cf --- /dev/null +++ b/Femto.Api/Controllers/Posts/Dto/AddPostReactionRequest.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Posts.Dto; + +public record AddPostReactionRequest(string Emoji); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/DeletePostReactionRequest.cs b/Femto.Api/Controllers/Posts/Dto/DeletePostReactionRequest.cs new file mode 100644 index 0000000..cb39e0e --- /dev/null +++ b/Femto.Api/Controllers/Posts/Dto/DeletePostReactionRequest.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Posts.Dto; + +public record DeletePostReactionRequest(string Emoji); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/PostsController.cs b/Femto.Api/Controllers/Posts/PostsController.cs index b24fc39..6036767 100644 --- a/Femto.Api/Controllers/Posts/PostsController.cs +++ b/Femto.Api/Controllers/Posts/PostsController.cs @@ -1,6 +1,8 @@ using Femto.Api.Controllers.Posts.Dto; using Femto.Common; using Femto.Modules.Blog.Application; +using Femto.Modules.Blog.Application.Commands.AddPostReaction; +using Femto.Modules.Blog.Application.Commands.ClearPostReaction; using Femto.Modules.Blog.Application.Commands.CreatePost; using Femto.Modules.Blog.Application.Commands.DeletePost; using Femto.Modules.Blog.Application.Queries.GetPosts; @@ -95,4 +97,27 @@ public class PostsController(IBlogModule blogModule, ICurrentUserContext current cancellationToken ); } + + [HttpPost("{postId}/reactions")] + [Authorize] + public async Task AddPostReaction(Guid postId, [FromBody] AddPostReactionRequest request, CancellationToken cancellationToken) + { + var currentUser = currentUserContext.CurrentUser!; + + await blogModule.Command(new AddPostReactionCommand(postId, request.Emoji, currentUser.Id), cancellationToken); + + return this.Ok(); + } + + [HttpDelete("{postId}/reactions")] + [Authorize] + public async Task DeletePostReaction(Guid postId, [FromBody] DeletePostReactionRequest request, CancellationToken cancellationToken) + { + var currentUser = currentUserContext.CurrentUser!; + + await blogModule.Command(new ClearPostReactionCommand(postId, request.Emoji, currentUser.Id), cancellationToken); + + return this.Ok(); + } + } diff --git a/Femto.Modules.Blog/Application/Commands/AddPostReaction/AddPostReactionCommand.cs b/Femto.Modules.Blog/Application/Commands/AddPostReaction/AddPostReactionCommand.cs new file mode 100644 index 0000000..9096687 --- /dev/null +++ b/Femto.Modules.Blog/Application/Commands/AddPostReaction/AddPostReactionCommand.cs @@ -0,0 +1,6 @@ +using Femto.Common; +using Femto.Common.Domain; + +namespace Femto.Modules.Blog.Application.Commands.AddPostReaction; + +public record AddPostReactionCommand(Guid PostId, string Emoji, Guid ReactorId) : ICommand; \ No newline at end of file diff --git a/Femto.Modules.Blog/Application/Commands/AddPostReaction/AddPostReactionCommandHandler.cs b/Femto.Modules.Blog/Application/Commands/AddPostReaction/AddPostReactionCommandHandler.cs new file mode 100644 index 0000000..e2c3f8a --- /dev/null +++ b/Femto.Modules.Blog/Application/Commands/AddPostReaction/AddPostReactionCommandHandler.cs @@ -0,0 +1,21 @@ +using Femto.Common.Domain; +using Microsoft.EntityFrameworkCore; + +namespace Femto.Modules.Blog.Application.Commands.AddPostReaction; + +internal class AddPostReactionCommandHandler(BlogContext context) + : ICommandHandler +{ + public async Task Handle(AddPostReactionCommand request, CancellationToken cancellationToken) + { + var post = await context.Posts.SingleOrDefaultAsync( + p => p.Id == request.PostId, + cancellationToken + ); + + if (post is null) + return; + + post.AddReaction(request.ReactorId, request.Emoji); + } +} diff --git a/Femto.Modules.Blog/Application/Commands/ClearPostReaction/ClearPostReactionCommand.cs b/Femto.Modules.Blog/Application/Commands/ClearPostReaction/ClearPostReactionCommand.cs new file mode 100644 index 0000000..618305f --- /dev/null +++ b/Femto.Modules.Blog/Application/Commands/ClearPostReaction/ClearPostReactionCommand.cs @@ -0,0 +1,6 @@ +using Femto.Common; +using Femto.Common.Domain; + +namespace Femto.Modules.Blog.Application.Commands.ClearPostReaction; + +public record ClearPostReactionCommand(Guid PostId, string Emoji, Guid ReactorId): ICommand; \ No newline at end of file diff --git a/Femto.Modules.Blog/Application/Commands/ClearPostReaction/ClearPostReactionCommandHandler.cs b/Femto.Modules.Blog/Application/Commands/ClearPostReaction/ClearPostReactionCommandHandler.cs new file mode 100644 index 0000000..e4d344e --- /dev/null +++ b/Femto.Modules.Blog/Application/Commands/ClearPostReaction/ClearPostReactionCommandHandler.cs @@ -0,0 +1,22 @@ +using Femto.Common.Domain; +using Femto.Modules.Blog.Application.Commands.AddPostReaction; +using Microsoft.EntityFrameworkCore; + +namespace Femto.Modules.Blog.Application.Commands.ClearPostReaction; + +internal class ClearPostReactionCommandHandler(BlogContext context) + : ICommandHandler +{ + public async Task Handle(ClearPostReactionCommand request, CancellationToken cancellationToken) + { + var post = await context.Posts.SingleOrDefaultAsync( + p => p.Id == request.PostId, + cancellationToken + ); + + if (post is null) + return; + + post.RemoveReaction(request.ReactorId, request.Emoji); + } +} diff --git a/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommandHandler.cs b/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommandHandler.cs index bfa6d31..2d9c713 100644 --- a/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommandHandler.cs +++ b/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommandHandler.cs @@ -25,9 +25,10 @@ internal class CreatePostCommandHandler(BlogContext context) media.Height )) .ToList() - ); - - post.IsPublic = request.IsPublic is true; + ) + { + IsPublic = request.IsPublic is true + }; await context.AddAsync(post, cancellationToken); diff --git a/Femto.Modules.Blog/Application/Configurations/PostConfiguration.cs b/Femto.Modules.Blog/Application/Configurations/PostConfiguration.cs index 40fbdbf..b1defec 100644 --- a/Femto.Modules.Blog/Application/Configurations/PostConfiguration.cs +++ b/Femto.Modules.Blog/Application/Configurations/PostConfiguration.cs @@ -10,10 +10,21 @@ internal class PostConfiguration : IEntityTypeConfiguration { table.ToTable("post"); table.OwnsMany(post => post.Media).WithOwner(); - table.OwnsMany(post => post.Reactions).WithOwner(); - - table.Property("PossibleReactionsJson") - .HasColumnName("possible_reactions"); + table.OwnsMany( + post => post.Reactions, + reactions => + { + reactions.WithOwner().HasForeignKey(r => r.PostId); + reactions.HasKey(r => new + { + r.PostId, + r.AuthorId, + r.Emoji, + }); + } + ); + + table.Property("PossibleReactionsJson").HasColumnName("possible_reactions"); table.Ignore(e => e.PossibleReactions); } diff --git a/Femto.Modules.Blog/Domain/Posts/Post.cs b/Femto.Modules.Blog/Domain/Posts/Post.cs index 2244cca..dc4d937 100644 --- a/Femto.Modules.Blog/Domain/Posts/Post.cs +++ b/Femto.Modules.Blog/Domain/Posts/Post.cs @@ -14,7 +14,7 @@ internal class Post : Entity public IList Reactions { get; private set; } = []; public bool IsPublic { get; set; } - + public DateTimeOffset PostedOn { get; private set; } private string PossibleReactionsJson { get; set; } = null!; @@ -38,4 +38,22 @@ internal class Post : Entity this.AddDomainEvent(new PostCreated(this)); } + + public void AddReaction(Guid reactorId, string emoji) + { + if (!this.PossibleReactions.Contains(emoji)) + return; + + if (this.Reactions.Any(r => r.AuthorId == reactorId && r.Emoji == emoji)) + return; + + this.Reactions.Add(new PostReaction(reactorId, this.Id, emoji)); + } + + public void RemoveReaction(Guid reactorId, string emoji) + { + this.Reactions = this + .Reactions.Where(r => r.AuthorId != reactorId || r.Emoji != emoji) + .ToList(); + } }