Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public async Task<Page<CommentResponse>> GetReplies(
[ProducesResponseType<ProblemDetails>(Status403Forbidden)]
[ProducesResponseType<ProblemDetails>(Status404NotFound)]
[ProducesResponseType<ProblemDetails>(Status500InternalServerError)]
public async Task<ActionResult<CommentResponse>> Reply([FromRoute] string commentId, [FromBody] ReplyRequest request)
public async Task<ActionResult<CommentResponse>> Reply([FromRoute] string commentId, [FromBody] CommentRequest request)
{
var response = await commentsService.ReplyToCommentAsync(commentId, User.GetRequiredUserId(), request.Content);
return CreatedAtAction(nameof(GetById), new { commentId = response.Id }, response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public async Task<Page<DiscussionResponse>> Search(
await discussionsService.SearchAsync(authorId, User.GetUserId(), offset, count);

/// <summary>
/// Retrieves top-level comments for a specified discussion with pagination.
/// Retrieves comments for a specified discussion with configurable tree traversal and pagination.
/// </summary>
/// <param name="discussionId">The unique identifier of the discussion.</param>
/// <param name="flatten">When true, returns all nested replies in a flat structure; otherwise returns direct children only.</param>
Expand All @@ -70,6 +70,7 @@ public async Task<Page<CommentResponse>> GetComments(
/// <summary>
/// Creates a new top-level comment in a discussion. Requires authenticated user and triggers real-time notifications.
/// </summary>
/// <param name="discussionId">The unique identifier of the discussion being commented.</param>
/// <param name="request">The comment content and target discussion identifier.</param>
/// <returns>The newly created top-level comment details.</returns>
[HttpPost("{discussionId}/comments"), Authorize]
Expand All @@ -78,12 +79,13 @@ public async Task<Page<CommentResponse>> GetComments(
[ProducesResponseType<ValidationProblemDetails>(Status400BadRequest)]
[ProducesResponseType<ProblemDetails>(Status403Forbidden)]
[ProducesResponseType<ProblemDetails>(Status500InternalServerError)]
public async Task<ActionResult<CommentResponse>> Reply([FromBody] CommentRequest request)
public async Task<ActionResult<CommentResponse>> Comment([FromRoute] string discussionId, [FromBody] CommentRequest request)
{
var response = await commentsService.ReplyToDiscussionAsync(request.DiscussionId, User.GetRequiredUserId(), request.Content);
var response = await commentsService.ReplyToDiscussionAsync(discussionId, User.GetRequiredUserId(), request.Content);

// TODO: handle exceptions and move to a better place
_ = commentHub.Clients
.Group(CommentsHub.GroupNames.NewCommentInDiscussion(request.DiscussionId))
.Group(CommentsHub.GroupNames.NewCommentInDiscussion(discussionId))
.SendCoreAsync(CommentsHub.Events.NewComment.ToString(), [response]);

return CreatedAtAction(nameof(CommentsController.GetById), "Comments", new { commentId = response.Id }, response);
Expand Down
2 changes: 1 addition & 1 deletion src/CrowdParlay.Social.Application/DTOs/CommentRequest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
namespace CrowdParlay.Social.Application.DTOs;

public record CommentRequest(string DiscussionId, string Content);
public record CommentRequest(string Content);
1 change: 1 addition & 0 deletions src/CrowdParlay.Social.Application/DTOs/CommentResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace CrowdParlay.Social.Application.DTOs;
public class CommentResponse
{
public required string Id { get; set; }
public required string SubjectId { get; set; }
public required string Content { get; set; }
public required AuthorResponse? Author { get; set; }
public required DateTimeOffset CreatedAt { get; set; }
Expand Down
3 changes: 0 additions & 3 deletions src/CrowdParlay.Social.Application/DTOs/ReplyRequest.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ private async Task<IEnumerable<CommentResponse>> EnrichAsync(IReadOnlyList<Comme
return comments.Select(comment => new CommentResponse
{
Id = comment.Id,
SubjectId = comment.SubjectId,
Content = comment.Content,
Author = authorsById[comment.AuthorId].Adapt<AuthorResponse>(),
CreatedAt = comment.CreatedAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ public async Task SetReactionsAsync(string subjectId, Guid authorId, ISet<string
throw new ForbiddenException("Such reaction set is not allowed.");

await subjectsRepository.SetReactionsAsync(subjectId, authorId, newReactions);

var reactionsToAdd = newReactions.Except(oldReactions).ToArray();
var reactionsToRemove = oldReactions.Except(newReactions).ToArray();
await subjectsRepository.UpdateReactionCountersAsync(subjectId, reactionsToAdd, reactionsToRemove);

var addedReactionsDiff = newReactions.Except(oldReactions).Select(reaction => new KeyValuePair<string, int>(reaction, 1));
var removedReactionsDiff = oldReactions.Except(newReactions).Select(reaction => new KeyValuePair<string, int>(reaction, -1));
var reactionsDiff = addedReactionsDiff.Concat(removedReactionsDiff).ToDictionary();
await subjectsRepository.UpdateReactionCountersAsync(subjectId, reactionsDiff);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ public interface ISubjectsRepository
{
public Task<ISet<string>> GetReactionsAsync(string subjectId, Guid authorId);
public Task SetReactionsAsync(string subjectId, Guid authorId, ISet<string> reactions);
public Task UpdateReactionCountersAsync(string subjectId, IEnumerable<string> reactionsToAdd, IEnumerable<string> reactionsToRemove);
public Task UpdateReactionCountersAsync(string subjectId, IDictionary<string, int> reactionsDiff);
public Task IncludeCommentInMetadataAsync(string discussionId, Guid authorId);
public Task ExcludeCommentFromMetadataAsync(string discussionId);

Expand Down
9 changes: 7 additions & 2 deletions src/CrowdParlay.Social.Domain/Entities/Comment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace CrowdParlay.Social.Domain.Entities;


[DebuggerDisplay("{Id} by {AuthorId} in reply to {SubjectId}")]
public class Comment
{
Expand All @@ -13,6 +12,12 @@ public class Comment
public required DateTimeOffset CreatedAt { get; set; }
public required int CommentCount { get; set; }
public required IList<Guid> LastCommentsAuthorIds { get; set; }
public required IDictionary<string, int> ReactionCounters { get; set; }
public required IList<string> ViewerReactions { get; set; }

private IDictionary<string, int> _reactionCounters = null!;
public required IDictionary<string, int> ReactionCounters
{
get => _reactionCounters;
set => _reactionCounters = value.Where(kv => kv.Value > 0).ToDictionary();
}
}
9 changes: 7 additions & 2 deletions src/CrowdParlay.Social.Domain/Entities/Discussion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace CrowdParlay.Social.Domain.Entities;


[DebuggerDisplay("{Id} by {AuthorId}")]
public class Discussion
{
Expand All @@ -13,6 +12,12 @@ public class Discussion
public required DateTimeOffset CreatedAt { get; set; }
public required int CommentCount { get; set; }
public required IList<Guid> LastCommentsAuthorIds { get; set; }
public required IDictionary<string, int> ReactionCounters { get; set; }
public required IList<string> ViewerReactions { get; set; }

private IDictionary<string, int> _reactionCounters = null!;
public required IDictionary<string, int> ReactionCounters
{
get => _reactionCounters;
set => _reactionCounters = value.Where(kv => kv.Value > 0).ToDictionary();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ public async Task<ISet<string>> GetReactionsAsync(string commentId, Guid authorI
public async Task SetReactionsAsync(string commentId, Guid authorId, ISet<string> reactions) =>
await _subjectsRepository.SetReactionsAsync(commentId, authorId, reactions);

public async Task UpdateReactionCountersAsync(string commentId, IEnumerable<string> reactionsToAdd, IEnumerable<string> reactionsToRemove) =>
await _subjectsRepository.UpdateReactionCountersAsync(commentId, reactionsToAdd, reactionsToRemove);
public async Task UpdateReactionCountersAsync(string commentId, IDictionary<string, int> reactionsDiff) =>
await _subjectsRepository.UpdateReactionCountersAsync(commentId, reactionsDiff);

private static Expression<Func<CommentDocument, Comment>> CreateCommentProjectionExpression(Guid? viewerId) => comment => new Comment
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ public async Task<ISet<string>> GetReactionsAsync(string discussionId, Guid auth
public async Task SetReactionsAsync(string discussionId, Guid authorId, ISet<string> reactions) =>
await _subjectsRepository.SetReactionsAsync(discussionId, authorId, reactions);

public async Task UpdateReactionCountersAsync(string subjectId, IEnumerable<string> reactionsToAdd, IEnumerable<string> reactionsToRemove) =>
await _subjectsRepository.UpdateReactionCountersAsync(subjectId, reactionsToAdd, reactionsToRemove);
public async Task UpdateReactionCountersAsync(string subjectId, IDictionary<string, int> reactionsDiff) =>
await _subjectsRepository.UpdateReactionCountersAsync(subjectId, reactionsDiff);

public async Task IncludeCommentInMetadataAsync(string discussionId, Guid authorId) =>
await _subjectsRepository.IncludeCommentInMetadataAsync(discussionId, authorId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public async Task<ISet<string>> GetReactionsAsync(string subjectId, Guid authorI
var pipeline = _subjects
.Find(session, subject => subject.Id == ObjectId.Parse(subjectId))
.Project(subject => subject.ReactionsByAuthorId.ContainsKey(authorId.ToString())
? subject.ReactionsByAuthorId[authorId.ToString()]
: new string[] { });
? subject.ReactionsByAuthorId[authorId.ToString()]
: new string[] { });

var reactions = await pipeline.FirstOrDefaultAsync() ?? throw new NotFoundException();
return reactions.ToHashSet();
Expand All @@ -38,16 +38,13 @@ public async Task SetReactionsAsync(string subjectId, Guid authorId, ISet<string
throw new NotFoundException();
}

public async Task UpdateReactionCountersAsync(string subjectId, IEnumerable<string> reactionsToAdd, IEnumerable<string> reactionsToRemove)
public async Task UpdateReactionCountersAsync(string subjectId, IDictionary<string, int> reactionsDiff)
{
var increments = reactionsToAdd.Select(reaction =>
Builders<TDocument>.Update.Inc(subject => subject.ReactionCounters[reaction], 1));

var decrements = reactionsToRemove.Select(reaction =>
Builders<TDocument>.Update.Inc(subject => subject.ReactionCounters[reaction], -1));
var updates = reactionsDiff.Select(kv =>
Builders<TDocument>.Update.Inc(subject => subject.ReactionCounters[kv.Key], kv.Value));

var filter = Builders<TDocument>.Filter.Eq(subject => subject.Id, ObjectId.Parse(subjectId));
var update = Builders<TDocument>.Update.Combine(increments.Union(decrements));
var update = Builders<TDocument>.Update.Combine(updates);
var result = await _subjects.UpdateOneAsync(session, filter, update);

if (result.MatchedCount == 0)
Expand Down Expand Up @@ -76,7 +73,7 @@ await _subjects

await _subjects.UpdateOneAsync(session, filter, update);
}

public async Task ExcludeCommentFromMetadataAsync(string subjectId)
{
var filter = Builders<TDocument>.Filter.Eq(subject => subject.Id, ObjectId.Parse(subjectId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace CrowdParlay.Social.IntegrationTests.Tests;

public class ReactionsTests(WebApplicationContext context) : IAssemblyFixture<WebApplicationContext>

Check warning on line 5 in tests/CrowdParlay.Social.IntegrationTests/Tests/ReactionsTests.cs

View workflow job for this annotation

GitHub Actions / Build & test

Fixture argument 'context' does not have a fixture source (if it comes from a collection definition, ensure the definition is in the same assembly as the test) (https://xunit.net/xunit.analyzers/rules/xUnit1041)

Check warning on line 5 in tests/CrowdParlay.Social.IntegrationTests/Tests/ReactionsTests.cs

View workflow job for this annotation

GitHub Actions / Publish API / Produce openapi.yaml

Fixture argument 'context' does not have a fixture source (if it comes from a collection definition, ensure the definition is in the same assembly as the test) (https://xunit.net/xunit.analyzers/rules/xUnit1041)
{
private readonly IServiceProvider _services = context.Services;

Expand Down Expand Up @@ -66,9 +66,7 @@
discussion.ViewerReactions.Should().BeEquivalentTo(woozyFace, redHeart);
discussion.ReactionCounters.Should().BeEquivalentTo(new Dictionary<string, int>
{
{ eggplant, 0 },
{ woozyFace, 1 },
{ nailPolish, 0 },
{ redHeart, 1 }
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace CrowdParlay.Social.IntegrationTests.Tests;

public class SignalRTests(WebApplicationContext context) : IAssemblyFixture<WebApplicationContext>

Check warning on line 9 in tests/CrowdParlay.Social.IntegrationTests/Tests/SignalRTests.cs

View workflow job for this annotation

GitHub Actions / Build & test

Fixture argument 'context' does not have a fixture source (if it comes from a collection definition, ensure the definition is in the same assembly as the test) (https://xunit.net/xunit.analyzers/rules/xUnit1041)

Check warning on line 9 in tests/CrowdParlay.Social.IntegrationTests/Tests/SignalRTests.cs

View workflow job for this annotation

GitHub Actions / Publish API / Produce openapi.yaml

Fixture argument 'context' does not have a fixture source (if it comes from a collection definition, ensure the definition is in the same assembly as the test) (https://xunit.net/xunit.analyzers/rules/xUnit1041)
{
private readonly IServiceProvider _services = context.Services;
private readonly HttpClient _client = context.Server.CreateClient();
Expand All @@ -24,6 +24,7 @@
var expectedComment = new CommentResponse
{
Id = ObjectId.GenerateNewId().ToString(),
SubjectId = discussionId,
Content = "Sample comment.",
Author = new AuthorResponse
{
Expand Down
Loading