feat: refine content calendar experience
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
@@ -14,7 +15,8 @@ public record CreateCommentRequest(
|
||||
Guid WorkspaceId,
|
||||
Guid ContentItemId,
|
||||
Guid? ParentCommentId,
|
||||
string Body);
|
||||
string Body,
|
||||
IFormFile? Attachment);
|
||||
|
||||
public class CreateCommentRequestValidator
|
||||
: Validator<CreateCommentRequest>
|
||||
@@ -23,13 +25,14 @@ public class CreateCommentRequestValidator
|
||||
{
|
||||
RuleFor(x => x.WorkspaceId).NotEmpty();
|
||||
RuleFor(x => x.ContentItemId).NotEmpty();
|
||||
RuleFor(x => x.Body).NotEmpty().MaximumLength(4000);
|
||||
RuleFor(x => x.Body).MaximumLength(4000);
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateCommentHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
IBlobStorage blobStorage,
|
||||
IContentItemActivityWriter activityWriter,
|
||||
INotificationEventWriter notificationEventWriter)
|
||||
: Endpoint<CreateCommentRequest, CommentDto>
|
||||
@@ -38,10 +41,19 @@ public class CreateCommentHandler(
|
||||
{
|
||||
Post("/api/comments");
|
||||
Options(o => o.WithTags("Comments"));
|
||||
AllowFileUploads();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CreateCommentRequest request, CancellationToken ct)
|
||||
{
|
||||
string body = request.Body?.Trim() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(body) && request.Attachment is null)
|
||||
{
|
||||
AddError(request => request.Body, "A comment body or attachment is required.");
|
||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
ContentItem? contentItem = await dbContext.ContentItems
|
||||
.SingleOrDefaultAsync(
|
||||
candidate => candidate.Id == request.ContentItemId && candidate.WorkspaceId == request.WorkspaceId,
|
||||
@@ -75,16 +87,70 @@ public class CreateCommentHandler(
|
||||
}
|
||||
}
|
||||
|
||||
Guid commentId = Guid.NewGuid();
|
||||
string? attachmentFileName = null;
|
||||
string? attachmentContentType = null;
|
||||
long? attachmentSizeBytes = null;
|
||||
string? attachmentBlobName = null;
|
||||
string? attachmentBlobUrl = null;
|
||||
|
||||
if (request.Attachment is not null)
|
||||
{
|
||||
string normalizedContentType = request.Attachment.ContentType.Trim().ToLowerInvariant();
|
||||
|
||||
if (request.Attachment.Length <= 0)
|
||||
{
|
||||
AddError(request => request.Attachment, "The attachment must not be empty.");
|
||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsInlineAttachmentContentType(normalizedContentType))
|
||||
{
|
||||
AddError(request => request.Attachment, "The attachment must be a PNG or JPEG image.");
|
||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
attachmentFileName = NormalizeFileName(request.Attachment.FileName, normalizedContentType);
|
||||
attachmentContentType = normalizedContentType;
|
||||
attachmentSizeBytes = request.Attachment.Length;
|
||||
attachmentBlobName =
|
||||
$"{contentItem.WorkspaceId}/{SubDirectoryNames.Contents}/{contentItem.Id}/comments/{commentId}/{attachmentFileName}";
|
||||
|
||||
try
|
||||
{
|
||||
attachmentBlobUrl = await blobStorage.UploadFileAsync(
|
||||
ContainerNames.Workspaces,
|
||||
attachmentBlobName,
|
||||
request.Attachment.OpenReadStream(),
|
||||
normalizedContentType,
|
||||
ct);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
AddError(request => request.Attachment, "The attachment file is invalid or unsupported.");
|
||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Comment comment = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Id = commentId,
|
||||
WorkspaceId = request.WorkspaceId,
|
||||
ContentItemId = request.ContentItemId,
|
||||
ParentCommentId = request.ParentCommentId,
|
||||
AuthorUserId = User.GetUserId(),
|
||||
AuthorDisplayName = User.GetAlias() ?? User.GetName(),
|
||||
AuthorEmail = User.GetEmail(),
|
||||
Body = request.Body.Trim(),
|
||||
Body = body,
|
||||
AttachmentFileName = attachmentFileName,
|
||||
AttachmentContentType = attachmentContentType,
|
||||
AttachmentSizeBytes = attachmentSizeBytes,
|
||||
AttachmentBlobContainerName = attachmentBlobName is not null ? ContainerNames.Workspaces : null,
|
||||
AttachmentBlobName = attachmentBlobName,
|
||||
AttachmentBlobUrl = attachmentBlobUrl,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
@@ -109,6 +175,7 @@ public class CreateCommentHandler(
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
parentCommentId = comment.ParentCommentId,
|
||||
attachmentFileName = comment.AttachmentFileName,
|
||||
})),
|
||||
ct);
|
||||
|
||||
@@ -135,8 +202,34 @@ public class CreateCommentHandler(
|
||||
comment.AuthorEmail,
|
||||
authorPortraitUrl,
|
||||
comment.Body,
|
||||
comment.AttachmentFileName,
|
||||
comment.AttachmentContentType,
|
||||
comment.AttachmentSizeBytes,
|
||||
comment.AttachmentBlobUrl,
|
||||
comment.CreatedAt);
|
||||
|
||||
await SendAsync(dto, StatusCodes.Status201Created, ct);
|
||||
}
|
||||
|
||||
private static bool IsInlineAttachmentContentType(string contentType)
|
||||
{
|
||||
return contentType.Trim().ToLowerInvariant() is "image/png" or "image/jpeg" or "image/jpg";
|
||||
}
|
||||
|
||||
private static string NormalizeFileName(string? fileName, string contentType)
|
||||
{
|
||||
string extension = contentType.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"image/png" => ".png",
|
||||
"image/jpeg" or "image/jpg" => ".jpg",
|
||||
_ => string.Empty,
|
||||
};
|
||||
string normalized = Path.GetFileName(fileName ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return $"comment-attachment{extension}";
|
||||
}
|
||||
|
||||
return normalized.Length > 256 ? normalized[..256] : normalized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@ public record CommentDto(
|
||||
string AuthorEmail,
|
||||
string? AuthorPortraitUrl,
|
||||
string Body,
|
||||
string? AttachmentFileName,
|
||||
string? AttachmentContentType,
|
||||
long? AttachmentSizeBytes,
|
||||
string? AttachmentBlobUrl,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
public class GetCommentsHandler(
|
||||
@@ -73,6 +77,10 @@ public class GetCommentsHandler(
|
||||
comment.AuthorEmail,
|
||||
authorPortraits.GetValueOrDefault(comment.AuthorUserId),
|
||||
comment.Body,
|
||||
comment.AttachmentFileName,
|
||||
comment.AttachmentContentType,
|
||||
comment.AttachmentSizeBytes,
|
||||
comment.AttachmentBlobUrl,
|
||||
comment.CreatedAt))
|
||||
.ToList();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user