diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml index 4f10cb4bd..dae0f3140 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml @@ -302,6 +302,88 @@ modules: notifications-service: localUrl: localhost:5006 url: notifications-service + + + communication: + path: /communication + routes: + - upstream: /chats + method: POST + use: downstream + downstream: communication-service/communication/chats + auth: true + + - upstream: /chats/{chatId}/messages + method: POST + use: downstream + downstream: communication-service/communication/chats/{chatId}/messages + auth: true + bind: + - chatId:{chatId} + + - upstream: /chats/{chatId}/users + method: PUT + use: downstream + downstream: communication-service/communication/chats/{chatId}/users + auth: true + bind: + - chatId:{chatId} + + - upstream: /chats/{chatId}/messages/{messageId}/status + method: PUT + use: downstream + downstream: communication-service/communication/chats/{chatId}/messages/{messageId}/status + auth: true + bind: + - chatId:{chatId} + - messageId:{messageId} + + - upstream: /chats/user/{userId} + method: GET + use: downstream + downstream: communication-service/communication/chats/user/{userId} + auth: true + bind: + - userId:{userId} + + - upstream: /chats/{chatId} + method: GET + use: downstream + downstream: communication-service/communication/chats/{chatId} + auth: true + bind: + - chatId:{chatId} + + - upstream: /chats/{chatId}/messages + method: GET + use: downstream + downstream: communication-service/communication/chats/{chatId}/messages + auth: true + bind: + - chatId:{chatId} + + - upstream: /chats/{chatId}/{userId} + method: DELETE + use: downstream + downstream: communication-service/communication/chats/{chatId}/{userId} + auth: true + bind: + - chatId:{chatId} + - userId:{userId} + + - upstream: /chats/{chatId}/messages/{messageId} + method: DELETE + use: downstream + downstream: communication-service/communication/chats/{chatId}/messages/{messageId} + auth: true + bind: + - chatId:{chatId} + - messageId:{messageId} + + services: + communication-service: + localUrl: localhost:5016 + url: communication-service students: @@ -349,6 +431,30 @@ modules: downstream: students-service/students/{studentId}/notifications auth: true + - upstream: /profiles/users/{userId}/views/paginated + method: GET + use: downstream + downstream: students-service/students/profiles/users/{userId}/views/paginated + auth: true + bind: + - userId:{userId} + + - upstream: /profiles/users/{userId}/views/viewed + method: GET + use: downstream + downstream: students-service/students/profiles/users/{userId}/views/viewed + auth: true + bind: + - userId:{userId} + + - upstream: /{blockerId}/blocked-users + method: GET + use: downstream + downstream: students-service/students/{blockerId}/blocked-users + auth: true + bind: + - blockerId:{blockerId} + - upstream: /{studentId} method: PUT use: downstream @@ -392,6 +498,32 @@ modules: downstream: students-service/students/{studentId}/notifications auth: true + - upstream: /profiles/users/{userProfileId}/view + method: POST + use: downstream + downstream: students-service/students/profiles/users/{userProfileId}/view + auth: true + bind: + - userProfileId:{userProfileId} + + - upstream: /{blockerId}/block-user/{blockedUserId} + method: POST + use: downstream + downstream: students-service/students/{blockerId}/block-user/{blockedUserId} + auth: true + bind: + - blockerId:{blockerId} + - blockedUserId:{blockedUserId} + + - upstream: /{blockerId}/unblock-user/{blockedUserId} + method: POST + use: downstream + downstream: students-service/students/{blockerId}/unblock-user/{blockedUserId} + auth: true + bind: + - blockerId:{blockerId} + - blockedUserId:{blockedUserId} + - upstream: /{studentId}/languages-and-interests method: PUT use: downstream @@ -454,6 +586,23 @@ modules: downstream: events-service/events/student/{studentId} auth: true + - upstream: /users/{userId}/feed + method: GET + use: downstream + downstream: events-service/events/users/{userId}/feed + auth: true + bind: + - userId:{userId} + + - upstream: /users/{userId}/views/paginated + method: GET + use: downstream + downstream: events-service/events/users/{userId}/views/paginated + auth: true + bind: + - userId:{userId} + + - upstream: /{eventId} method: DELETE use: downstream @@ -483,6 +632,14 @@ modules: downstream: events-service/events/{eventId}/show-interest auth: true + - upstream: /{eventId}/view + method: POST + use: downstream + downstream: events-service/events/{eventId}/view + auth: true + bind: + - eventId:{eventId} + - upstream: /{eventId}/show-interest method: DELETE use: downstream @@ -590,7 +747,7 @@ modules: auth: true - upstream: /search - method: POST + method: GET use: downstream downstream: comments-service/comments/search @@ -615,11 +772,21 @@ modules: downstream: reactions-service/reactions auth: true + - upstream: /{reactionId} + method: PUT + use: downstream + downstream: reactions-service/reactions/{reactionId} + auth: true + bind: + - reactionId: {reactionId} + - upstream: /{reactionId} method: DELETE use: downstream downstream: reactions-service/reactions/{reactionId} auth: true + bind: + - reactionId: {reactionId} - upstream: / method: GET @@ -747,6 +914,22 @@ modules: downstream: friends-service/friends/requests/sent/{userId} auth: true + - upstream: /{userId}/followers + method: GET + use: downstream + downstream: friends-service/friends/{userId}/followers + auth: true + bind: + - userId: {userId} + + - upstream: /{userId}/following + method: GET + use: downstream + downstream: friends-service/friends/{userId}/following + auth: true + bind: + - userId: {userId} + - upstream: /requests/{userId}/withdraw method: PUT use: downstream @@ -771,6 +954,14 @@ modules: downstream: posts-service/posts auth: true + - upstream: /users/{userId}/feed + method: GET + use: downstream + downstream: posts-service/posts/users/{userId}/feed + bind: + - userId:{userId} + auth: true + - upstream: /search method: GET use: downstream @@ -798,6 +989,8 @@ modules: method: GET use: downstream downstream: posts-service/posts/{postId} + bind: + - postId:{postId} - upstream: / method: GET @@ -898,7 +1091,7 @@ modules: - upstream: /users/{userId}/organizations method: GET use: downstream - downstream: organizations-service/users/{userId}/organizations + downstream: organizations-service/organizations/users/{userId}/organizations auth: true bind: - userId: {userId} @@ -910,6 +1103,16 @@ modules: auth: true bind: - organizationId: {organizationId} + + - upstream: /users/{userId}/organizations/follow + method: GET + use: downstream + downstream: organizations-service/organizations/users/{userId}/organizations/follow + auth: true + bind: + - userId: {userId} + + - upstream: /{organizationId}/details/gallery-users method: GET @@ -926,6 +1129,13 @@ modules: bind: - organizationId: {organizationId} + - upstream: /{organizationId}/requests + method: GET + use: downstream + downstream: organizations-service/organizations/{organizationId}/requests + bind: + - organizationId: {organizationId} + - upstream: /paginated method: GET use: downstream diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/MiniSpace.Services.Comments.Api.sln b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/MiniSpace.Services.Comments.Api.sln new file mode 100644 index 000000000..62a580626 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/MiniSpace.Services.Comments.Api.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Comments.Api", "MiniSpace.Services.Comments.Api.csproj", "{A198F9BC-F912-464E-BBCB-851CFBDD320F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A198F9BC-F912-464E-BBCB-851CFBDD320F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A198F9BC-F912-464E-BBCB-851CFBDD320F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A198F9BC-F912-464E-BBCB-851CFBDD320F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A198F9BC-F912-464E-BBCB-851CFBDD320F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {331542E3-F8A0-4711-AC84-18DD6B0D3896} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/Program.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/Program.cs index f50e68cd7..60fe637b4 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/Program.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Api/Program.cs @@ -17,6 +17,7 @@ using MiniSpace.Services.Comments.Application.Queries; using MiniSpace.Services.Comments.Application.Services; using MiniSpace.Services.Comments.Infrastructure; +using MiniSpace.Services.Comments.Core.Wrappers; namespace MiniSpace.Services.Identity.Api { @@ -34,14 +35,10 @@ public static async Task Main(string[] args) .Configure(app => app .UseInfrastructure() .UseEndpoints(endpoints => endpoints - .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) - .Post("comments/search", async (cmd, ctx) => - { - var pagedResult = await ctx.RequestServices.GetService().BrowseCommentsAsync(cmd); - await ctx.Response.WriteJsonAsync(pagedResult); - })) + .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name))) .UseDispatcherEndpoints(endpoints => endpoints .Get("comments/{commentID}") + .Get>("comments/search") .Post("comments") .Put("comments/{commentID}") .Delete("comments/{commentID}") diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/AddLike.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/AddLike.cs index 440c96dc4..438a62314 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/AddLike.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/AddLike.cs @@ -5,11 +5,15 @@ namespace MiniSpace.Services.Comments.Application.Commands { public class AddLike : ICommand { - public Guid CommentId { get; set; } + public Guid CommentId { get; } + public Guid UserId { get; } + public string CommentContext { get; } - public AddLike(Guid commentId) + public AddLike(Guid commentId, Guid userId, string commentContext) { CommentId = commentId; + UserId = userId; + CommentContext = commentContext; } } } diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/AddLikeHandler.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/AddLikeHandler.cs index abd04f183..75ceb747f 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/AddLikeHandler.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/AddLikeHandler.cs @@ -1,48 +1,111 @@ using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Convey.CQRS.Commands; using MiniSpace.Services.Comments.Application.Events; using MiniSpace.Services.Comments.Application.Exceptions; -using MiniSpace.Services.Comments.Application.Services; +using MiniSpace.Services.Comments.Application.Services.Clients; using MiniSpace.Services.Comments.Core.Entities; using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Core.Exceptions; +using MiniSpace.Services.Comments.Application.Services; +using System.Text.Json; namespace MiniSpace.Services.Comments.Application.Commands.Handlers { public class AddLikeHandler : ICommandHandler { - private readonly ICommentRepository _commentRepository; - private readonly IAppContext _appContext; + private readonly IOrganizationEventsCommentRepository _organizationEventsCommentRepository; + private readonly IOrganizationPostsCommentRepository _organizationPostsCommentRepository; + private readonly IUserEventsCommentRepository _userEventsCommentRepository; + private readonly IUserPostsCommentRepository _userPostsCommentRepository; private readonly IMessageBroker _messageBroker; + private readonly IAppContext _appContext; + private readonly IStudentsServiceClient _userServiceClient; - public AddLikeHandler(ICommentRepository commentRepository, IAppContext appContext, - IMessageBroker messageBroker) + public AddLikeHandler( + IOrganizationEventsCommentRepository organizationEventsCommentRepository, + IOrganizationPostsCommentRepository organizationPostsCommentRepository, + IUserEventsCommentRepository userEventsCommentRepository, + IUserPostsCommentRepository userPostsCommentRepository, + IMessageBroker messageBroker, + IAppContext appContext, + IStudentsServiceClient userServiceClient) { - _commentRepository = commentRepository; - _appContext = appContext; + _organizationEventsCommentRepository = organizationEventsCommentRepository; + _organizationPostsCommentRepository = organizationPostsCommentRepository; + _userEventsCommentRepository = userEventsCommentRepository; + _userPostsCommentRepository = userPostsCommentRepository; _messageBroker = messageBroker; + _appContext = appContext; + _userServiceClient = userServiceClient; } public async Task HandleAsync(AddLike command, CancellationToken cancellationToken = default) { - var comment = await _commentRepository.GetAsync(command.CommentId); - if (comment is null) + var commandJson = JsonSerializer.Serialize(command, new JsonSerializerOptions { WriteIndented = true }); + Console.WriteLine($"Received AddLike command: {commandJson}"); + var identity = _appContext.Identity; + + if (!identity.IsAuthenticated || identity.Id != command.UserId) { - throw new CommentNotFoundException(command.CommentId); + throw new UnauthorizedCommentAccessException(command.CommentId, identity.Id); } - var identity = _appContext.Identity; - if (!identity.IsAuthenticated) + var user = await _userServiceClient.GetAsync(command.UserId); + if (user == null) { - throw new UnauthorizedCommentAccessException(command.CommentId, identity.Id); + throw new UserNotFoundException(command.UserId); + } + + if (!Enum.TryParse(command.CommentContext, true, out var commentContext)) + { + throw new InvalidCommentContextEnumException(command.CommentContext); } - comment.Like(identity.Id); - await _commentRepository.UpdateAsync(comment); + Comment comment = await GetCommentAsync(command.CommentId, commentContext); + if (comment == null) + { + throw new CommentNotFoundException(command.CommentId); + } + + comment.Like(command.UserId); + await UpdateCommentAsync(comment, commentContext); await _messageBroker.PublishAsync(new CommentUpdated(command.CommentId)); } + + private async Task GetCommentAsync(Guid commentId, CommentContext context) + { + return context switch + { + CommentContext.OrganizationEvent => await _organizationEventsCommentRepository.GetAsync(commentId), + CommentContext.OrganizationPost => await _organizationPostsCommentRepository.GetAsync(commentId), + CommentContext.UserEvent => await _userEventsCommentRepository.GetAsync(commentId), + CommentContext.UserPost => await _userPostsCommentRepository.GetAsync(commentId), + _ => null + }; + } + + private async Task UpdateCommentAsync(Comment comment, CommentContext context) + { + switch (context) + { + case CommentContext.OrganizationEvent: + await _organizationEventsCommentRepository.UpdateAsync(comment); + break; + case CommentContext.OrganizationPost: + await _organizationPostsCommentRepository.UpdateAsync(comment); + break; + case CommentContext.UserEvent: + await _userEventsCommentRepository.UpdateAsync(comment); + break; + case CommentContext.UserPost: + await _userPostsCommentRepository.UpdateAsync(comment); + break; + default: + throw new InvalidCommentContextEnumException(context.ToString()); + } + } } } diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/CreateCommentHandler.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/CreateCommentHandler.cs index 4481b83a8..bacf66423 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/CreateCommentHandler.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/Handlers/CreateCommentHandler.cs @@ -48,6 +48,16 @@ public CreateCommentHandler( public async Task HandleAsync(CreateComment command, CancellationToken cancellationToken = default) { var identity = _appContext.Identity; + + if (identity.IsAuthenticated) + { + Console.WriteLine($"User is authenticated: {identity.Id}"); + Console.WriteLine($"User command id is: {command.UserId}"); + } + else + { + Console.WriteLine("User is not authenticated."); + } if (identity.IsAuthenticated && identity.Id != command.UserId) { throw new UnauthorizedCommentAccessException(command.ContextId, identity.Id); diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/SearchComments.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/SearchComments.cs deleted file mode 100644 index 34e9bf540..000000000 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Commands/SearchComments.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Convey.CQRS.Commands; -using MiniSpace.Services.Comments.Application.Dto; - -namespace MiniSpace.Services.Comments.Application.Commands -{ - public class SearchComments : ICommand - { - public Guid ContextId { get; set; } - public string CommentContext { get; set; } - public Guid ParentId { get; set; } - public PageableDto Pageable { get; set; } - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Queries/SearchComments.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Queries/SearchComments.cs new file mode 100644 index 000000000..6f49f714c --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Queries/SearchComments.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Convey.CQRS.Queries; +using MiniSpace.Services.Comments.Application.Dto; +using MiniSpace.Services.Comments.Core.Wrappers; + +namespace MiniSpace.Services.Comments.Application.Queries +{ + public class SearchComments : IQuery> + { + public Guid ContextId { get; set; } + public string CommentContext { get; set; } + public Guid? ParentId { get; set; } + public int Page { get; set; } + public int Size { get; set; } + public string SortBy { get; set; } // Changed to string to handle the incoming JSON properly + public string Direction { get; set; } + + // This property will split the SortBy string into an array + public IEnumerable SortByArray => + string.IsNullOrEmpty(SortBy) ? new List { "CreatedAt" } : SortBy.Split(','); + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Services/ICommentService.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Services/ICommentService.cs deleted file mode 100644 index 0124f30e6..000000000 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Application/Services/ICommentService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using MiniSpace.Services.Comments.Application.Commands; -using MiniSpace.Services.Comments.Application.Dto; -using MiniSpace.Services.Comments.Application.Wrappers; -using MiniSpace.Services.Comments.Core.Wrappers; - -namespace MiniSpace.Services.Comments.Application.Services -{ - public interface ICommentService - { - Task> BrowseCommentsAsync(SearchComments command); - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/Comment.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/Comment.cs index 328c55b9f..92384badd 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/Comment.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Core/Entities/Comment.cs @@ -8,8 +8,8 @@ namespace MiniSpace.Services.Comments.Core.Entities { public class Comment : AggregateRoot { - private ISet _likes = new HashSet(); - private ISet _replies = new HashSet(); + private readonly ISet _likes = new HashSet(); + private readonly ISet _replies = new HashSet(); public Guid ContextId { get; private set; } public CommentContext CommentContext { get; private set; } @@ -22,17 +22,10 @@ public class Comment : AggregateRoot public int RepliesCount => _replies.Count; public bool IsDeleted { get; private set; } - public IEnumerable Likes - { - get => _likes; - private set => _likes = new HashSet(value); - } + // Expose the _likes set as a read-only IEnumerable + public IEnumerable Likes => _likes; - public IEnumerable Replies - { - get => _replies; - private set => _replies = new HashSet(value); - } + public IEnumerable Replies => _replies; public Comment(Guid id, Guid contextId, CommentContext commentContext, Guid userId, IEnumerable likes, Guid parentId, string textContent, DateTime createdAt, DateTime lastUpdatedAt, @@ -42,19 +35,19 @@ public Comment(Guid id, Guid contextId, CommentContext commentContext, Guid user ContextId = contextId; CommentContext = commentContext; UserId = userId; - Likes = likes; + _likes = new HashSet(likes); ParentId = parentId; TextContent = textContent; CreatedAt = createdAt; LastUpdatedAt = lastUpdatedAt; LastReplyAt = lastReplyAt; - Replies = replies; + _replies = new HashSet(replies); IsDeleted = isDeleted; } public void Like(Guid userId) { - if (Likes.Any(id => id == userId)) + if (_likes.Contains(userId)) { throw new UserAlreadyLikesCommentException(userId); } @@ -65,7 +58,7 @@ public void Like(Guid userId) public void UnLike(Guid userId) { - if (Likes.All(id => id != userId)) + if (!_likes.Contains(userId)) { throw new UserNotLikeCommentException(userId, Id); } diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Extensions.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Extensions.cs index 1d997e6d8..38ac1c4c9 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Extensions.cs @@ -39,6 +39,8 @@ using MiniSpace.Services.Comments.Infrastructure.Mongo.Repositories; using MiniSpace.Services.Comments.Infrastructure.Services; using System.Diagnostics.CodeAnalysis; +using MiniSpace.Services.Comments.Application.Services.Clients; +using MiniSpace.Services.Comments.Infrastructure.Services.Clients; namespace MiniSpace.Services.Comments.Infrastructure { @@ -53,9 +55,10 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(ctx => ctx.GetRequiredService().Create()); diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/Extensions.cs index 7f87c8a54..0c1c1e1f7 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Documents/Extensions.cs @@ -79,6 +79,25 @@ public static ReplyDocument ToDocument(this Reply entity) TextContent = entity.TextContent, CreatedAt = entity.CreatedAt }; + + public static CommentDto AsDto(this Comment comment) + { + return new CommentDto + { + Id = comment.Id, + ContextId = comment.ContextId, + CommentContext = comment.CommentContext.ToString().ToLowerInvariant(), + UserId = comment.UserId, + Likes = comment.Likes, + ParentId = comment.ParentId, + TextContent = comment.TextContent, + CreatedAt = comment.CreatedAt, + LastUpdatedAt = comment.LastUpdatedAt, + LastReplyAt = comment.LastReplyAt, + RepliesCount = comment.Replies.Count(), + IsDeleted = comment.IsDeleted + }; + } public static OrganizationEventCommentDocument ToOrganizationEventDocument(this IEnumerable comments, Guid organizationEventId, Guid organizationId) => new OrganizationEventCommentDocument diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Queries/Handlers/SearchCommentsHandler.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Queries/Handlers/SearchCommentsHandler.cs new file mode 100644 index 000000000..388ff24a3 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Queries/Handlers/SearchCommentsHandler.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Comments.Application.Dto; +using MiniSpace.Services.Comments.Application.Queries; +using MiniSpace.Services.Comments.Core.Entities; +using MiniSpace.Services.Comments.Core.Repositories; +using MiniSpace.Services.Comments.Core.Wrappers; +using MiniSpace.Services.Comments.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Comments.Infrastructure.Mongo.Queries.Handlers +{ + public class SearchCommentsHandler : IQueryHandler> + { + private readonly IOrganizationEventsCommentRepository _organizationEventsRepository; + private readonly IOrganizationPostsCommentRepository _organizationPostsRepository; + private readonly IUserEventsCommentRepository _userEventsRepository; + private readonly IUserPostsCommentRepository _userPostsRepository; + + public SearchCommentsHandler( + IOrganizationEventsCommentRepository organizationEventsRepository, + IOrganizationPostsCommentRepository organizationPostsRepository, + IUserEventsCommentRepository userEventsRepository, + IUserPostsCommentRepository userPostsRepository) + { + _organizationEventsRepository = organizationEventsRepository; + _organizationPostsRepository = organizationPostsRepository; + _userEventsRepository = userEventsRepository; + _userPostsRepository = userPostsRepository; + } + + public async Task> HandleAsync(SearchComments query, CancellationToken cancellationToken) +{ + try + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + // Initialize the list to hold all comments from all repositories + var allComments = new List(); + + // Use the new SortByArray property + var sortByList = query.SortByArray.ToList(); + + CommentContext contextEnum; + if (!Enum.TryParse(query.CommentContext, true, out contextEnum)) + { + throw new ArgumentException($"Invalid CommentContext value: {query.CommentContext}"); + } + + var browseRequest = new BrowseCommentsRequest( + pageNumber: query.Page, + pageSize: query.Size, + contextId: query.ContextId, + context: contextEnum, + parentId: query.ParentId ?? Guid.Empty, + sortBy: sortByList, + sortDirection: query.Direction + ); + + // Logging the browseRequest for debug purposes + Console.WriteLine($"Searching with ContextId: {query.ContextId}, CommentContext: {query.CommentContext}"); + + // Search in OrganizationEventsCommentRepository + Console.WriteLine("Searching in OrganizationEventsCommentRepository..."); + var orgEventsComments = await _organizationEventsRepository.BrowseCommentsAsync(browseRequest); + if (orgEventsComments?.Items != null && orgEventsComments.Items.Any()) + { + Console.WriteLine($"Found {orgEventsComments.Items.Count()} comments in OrganizationEventsCommentRepository."); + allComments.AddRange(orgEventsComments.Items); + } + else + { + Console.WriteLine("No comments found in OrganizationEventsCommentRepository."); + } + + // Search in OrganizationPostsCommentRepository + Console.WriteLine("Searching in OrganizationPostsCommentRepository..."); + var orgPostsComments = await _organizationPostsRepository.BrowseCommentsAsync(browseRequest); + if (orgPostsComments?.Items != null && orgPostsComments.Items.Any()) + { + Console.WriteLine($"Found {orgPostsComments.Items.Count()} comments in OrganizationPostsCommentRepository."); + allComments.AddRange(orgPostsComments.Items); + } + else + { + Console.WriteLine("No comments found in OrganizationPostsCommentRepository."); + } + + // Search in UserEventsCommentRepository + Console.WriteLine("Searching in UserEventsCommentRepository..."); + var userEventsComments = await _userEventsRepository.BrowseCommentsAsync(browseRequest); + if (userEventsComments?.Items != null && userEventsComments.Items.Any()) + { + Console.WriteLine($"Found {userEventsComments.Items.Count()} comments in UserEventsCommentRepository."); + allComments.AddRange(userEventsComments.Items); + } + else + { + Console.WriteLine("No comments found in UserEventsCommentRepository."); + } + + // Search in UserPostsCommentRepository + Console.WriteLine("Searching in UserPostsCommentRepository..."); + var userPostsComments = await _userPostsRepository.BrowseCommentsAsync(browseRequest); + if (userPostsComments?.Items != null && userPostsComments.Items.Any()) + { + Console.WriteLine($"Found {userPostsComments.Items.Count()} comments in UserPostsCommentRepository."); + allComments.AddRange(userPostsComments.Items); + } + else + { + Console.WriteLine("No comments found in UserPostsCommentRepository."); + } + + if (!allComments.Any()) + { + Console.WriteLine("No comments found in any repository."); + } + + // Sort and paginate the aggregated comments + var sortedComments = sortByList.Contains("CreatedAt") + ? allComments.OrderByDescending(c => c.CreatedAt).ToList() + : allComments.OrderBy(c => c.Id).ToList(); // default sorting + + var pagedComments = sortedComments + .Skip((query.Page - 1) * query.Size) + .Take(query.Size) + .ToList(); + + var totalItems = allComments.Count; + var commentDtos = pagedComments.Select(c => c.AsDto()).ToList(); + + var response = new PagedResponse(commentDtos, query.Page, query.Size, totalItems); + + // Log the response to the console + Console.WriteLine($"Response: {System.Text.Json.JsonSerializer.Serialize(response)}"); + + return response; + } + catch (Exception ex) + { + // Log exception with more context + Console.Error.WriteLine($"Error in SearchCommentsHandler: {ex}"); + throw; + } +} + + } +} diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/OrganizationEventsCommentRepository.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/OrganizationEventsCommentRepository.cs index 1d595f634..e662efd4e 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/OrganizationEventsCommentRepository.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/OrganizationEventsCommentRepository.cs @@ -30,11 +30,24 @@ public async Task GetAsync(Guid id) public async Task AddAsync(Comment comment) { var filter = Builders.Filter.Eq(d => d.OrganizationEventId, comment.ContextId); - var update = Builders.Update.Push(d => d.Comments, comment.ToDocument()); - await _repository.Collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + var update = Builders.Update.Combine( + Builders.Update.Push(d => d.Comments, comment.ToDocument()), + Builders.Update.SetOnInsert(d => d.OrganizationEventId, comment.ContextId), + Builders.Update.SetOnInsert(d => d.Id, Guid.NewGuid()) + ); + + var options = new UpdateOptions { IsUpsert = true }; + var result = await _repository.Collection.UpdateOneAsync(filter, update, options); + + if (!result.IsAcknowledged || result.ModifiedCount == 0) + { + // Handle the case where the update or insert did not succeed + Console.Error.WriteLine("Failed to add or update the comment."); + } } + public async Task UpdateAsync(Comment comment) { var filter = Builders.Filter.And( @@ -45,10 +58,12 @@ public async Task UpdateAsync(Comment comment) var update = Builders.Update .Set($"{nameof(OrganizationEventCommentDocument.Comments)}.$.{nameof(CommentDocument.TextContent)}", comment.TextContent) .Set($"{nameof(OrganizationEventCommentDocument.Comments)}.$.{nameof(CommentDocument.LastUpdatedAt)}", comment.LastUpdatedAt) - .Set($"{nameof(OrganizationEventCommentDocument.Comments)}.$.{nameof(CommentDocument.IsDeleted)}", comment.IsDeleted); + .Set($"{nameof(OrganizationEventCommentDocument.Comments)}.$.{nameof(CommentDocument.IsDeleted)}", comment.IsDeleted) + .Set($"{nameof(OrganizationEventCommentDocument.Comments)}.$.{nameof(CommentDocument.Likes)}", comment.Likes); await _repository.Collection.UpdateOneAsync(filter, update); } + public async Task DeleteAsync(Guid id) { diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/OrganizationPostsCommentRepository.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/OrganizationPostsCommentRepository.cs index 13988ceed..268ff6e1d 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/OrganizationPostsCommentRepository.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/OrganizationPostsCommentRepository.cs @@ -30,9 +30,20 @@ public async Task GetAsync(Guid id) public async Task AddAsync(Comment comment) { var filter = Builders.Filter.Eq(d => d.OrganizationPostId, comment.ContextId); - var update = Builders.Update.Push(d => d.Comments, comment.ToDocument()); - await _repository.Collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + var update = Builders.Update.Combine( + Builders.Update.Push(d => d.Comments, comment.ToDocument()), + Builders.Update.SetOnInsert(d => d.OrganizationPostId, comment.ContextId), + Builders.Update.SetOnInsert(d => d.Id, Guid.NewGuid()) + ); + + var options = new UpdateOptions { IsUpsert = true }; + var result = await _repository.Collection.UpdateOneAsync(filter, update, options); + + if (!result.IsAcknowledged || result.ModifiedCount == 0) + { + Console.Error.WriteLine("Failed to add or update the comment."); + } } public async Task UpdateAsync(Comment comment) @@ -45,7 +56,8 @@ public async Task UpdateAsync(Comment comment) var update = Builders.Update .Set($"{nameof(OrganizationPostCommentDocument.Comments)}.$.{nameof(CommentDocument.TextContent)}", comment.TextContent) .Set($"{nameof(OrganizationPostCommentDocument.Comments)}.$.{nameof(CommentDocument.LastUpdatedAt)}", comment.LastUpdatedAt) - .Set($"{nameof(OrganizationPostCommentDocument.Comments)}.$.{nameof(CommentDocument.IsDeleted)}", comment.IsDeleted); + .Set($"{nameof(OrganizationPostCommentDocument.Comments)}.$.{nameof(CommentDocument.IsDeleted)}", comment.IsDeleted) + .Set($"{nameof(OrganizationPostCommentDocument.Comments)}.$.{nameof(CommentDocument.Likes)}", comment.Likes); await _repository.Collection.UpdateOneAsync(filter, update); } @@ -67,17 +79,17 @@ public async Task> GetByPostIdAsync(Guid postId) public async Task> BrowseCommentsAsync(BrowseCommentsRequest request) { var filterDefinition = Builders.Filter.Eq(d => d.OrganizationPostId, request.ContextId); - var sortDefinition = OrganizationPostCommentExtensions.ToSortDefinition(request.SortBy, request.SortDirection); - var pagedEvents = await _repository.Collection.AggregateByPage( - filterDefinition, - sortDefinition, - request.PageNumber, - request.PageSize - ); + var document = await _repository.Collection.Find(filterDefinition).FirstOrDefaultAsync(); + + if (document == null) + { + return new PagedResponse(Enumerable.Empty(), request.PageNumber, request.PageSize, 0); + } - var comments = pagedEvents.data.SelectMany(d => d.Comments.Select(c => c.AsEntity())); - return new PagedResponse(comments, request.PageNumber, request.PageSize, pagedEvents.totalElements); + var comments = document.Comments.Select(c => c.AsEntity()).ToList(); + + return new PagedResponse(comments, request.PageNumber, request.PageSize, comments.Count); } } } diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/UserEventsCommentRepository.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/UserEventsCommentRepository.cs index c82166a86..53591739a 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/UserEventsCommentRepository.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/UserEventsCommentRepository.cs @@ -30,9 +30,21 @@ public async Task GetAsync(Guid id) public async Task AddAsync(Comment comment) { var filter = Builders.Filter.Eq(d => d.UserEventId, comment.ContextId); - var update = Builders.Update.Push(d => d.Comments, comment.ToDocument()); - await _repository.Collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + var update = Builders.Update.Combine( + Builders.Update.Push(d => d.Comments, comment.ToDocument()), + Builders.Update.SetOnInsert(d => d.UserEventId, comment.ContextId), + Builders.Update.SetOnInsert(d => d.Id, Guid.NewGuid()) + ); + + var options = new UpdateOptions { IsUpsert = true }; + var result = await _repository.Collection.UpdateOneAsync(filter, update, options); + + if (!result.IsAcknowledged || result.ModifiedCount == 0) + { + // Handle the case whre the update or insert did not succeed + Console.Error.WriteLine("Failed to add or update the comment."); + } } public async Task UpdateAsync(Comment comment) @@ -45,7 +57,8 @@ public async Task UpdateAsync(Comment comment) var update = Builders.Update .Set($"{nameof(UserEventCommentDocument.Comments)}.$.{nameof(CommentDocument.TextContent)}", comment.TextContent) .Set($"{nameof(UserEventCommentDocument.Comments)}.$.{nameof(CommentDocument.LastUpdatedAt)}", comment.LastUpdatedAt) - .Set($"{nameof(UserEventCommentDocument.Comments)}.$.{nameof(CommentDocument.IsDeleted)}", comment.IsDeleted); + .Set($"{nameof(UserEventCommentDocument.Comments)}.$.{nameof(CommentDocument.IsDeleted)}", comment.IsDeleted) + .Set($"{nameof(UserEventCommentDocument.Comments)}.$.{nameof(CommentDocument.Likes)}", comment.Likes); await _repository.Collection.UpdateOneAsync(filter, update); } diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/UserPostsCommentRepository.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/UserPostsCommentRepository.cs index 7084b0335..3edf48cba 100644 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/UserPostsCommentRepository.cs +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Mongo/Repositories/UserPostsCommentRepository.cs @@ -30,11 +30,22 @@ public async Task GetAsync(Guid id) public async Task AddAsync(Comment comment) { var filter = Builders.Filter.Eq(d => d.UserPostId, comment.ContextId); - var update = Builders.Update.Push(d => d.Comments, comment.ToDocument()); - await _repository.Collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + var update = Builders.Update.Combine( + Builders.Update.Push(d => d.Comments, comment.ToDocument()), + Builders.Update.SetOnInsert(d => d.UserPostId, comment.ContextId), + Builders.Update.SetOnInsert(d => d.Id, Guid.NewGuid()) + ); + + var options = new UpdateOptions { IsUpsert = true }; + var result = await _repository.Collection.UpdateOneAsync(filter, update, options); + + if (!result.IsAcknowledged || result.ModifiedCount == 0) + { + } } + public async Task UpdateAsync(Comment comment) { var filter = Builders.Filter.And( @@ -45,7 +56,8 @@ public async Task UpdateAsync(Comment comment) var update = Builders.Update .Set($"{nameof(UserPostCommentDocument.Comments)}.$.{nameof(CommentDocument.TextContent)}", comment.TextContent) .Set($"{nameof(UserPostCommentDocument.Comments)}.$.{nameof(CommentDocument.LastUpdatedAt)}", comment.LastUpdatedAt) - .Set($"{nameof(UserPostCommentDocument.Comments)}.$.{nameof(CommentDocument.IsDeleted)}", comment.IsDeleted); + .Set($"{nameof(UserPostCommentDocument.Comments)}.$.{nameof(CommentDocument.IsDeleted)}", comment.IsDeleted) + .Set($"{nameof(UserPostCommentDocument.Comments)}.$.{nameof(CommentDocument.Likes)}", comment.Likes); await _repository.Collection.UpdateOneAsync(filter, update); } diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/Clients/StudentsServiceClient.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/Clients/StudentsServiceClient.cs new file mode 100644 index 000000000..81e3883d4 --- /dev/null +++ b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/Clients/StudentsServiceClient.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using Convey.HTTP; +using MiniSpace.Services.Comments.Application.Dto; +using MiniSpace.Services.Comments.Application.Services.Clients; + +namespace MiniSpace.Services.Comments.Infrastructure.Services.Clients +{ + [ExcludeFromCodeCoverage] + public class StudentsServiceClient : IStudentsServiceClient + { + private readonly IHttpClient _httpClient; + private readonly string _url; + + public StudentsServiceClient(IHttpClient httpClient, HttpClientOptions options) + { + _httpClient = httpClient; + _url = options.Services["students"]; + } + + public Task GetAsync(Guid id) + => _httpClient.GetAsync($"{_url}/students/{id}"); + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/CommentService.cs b/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/CommentService.cs deleted file mode 100644 index 557809844..000000000 --- a/MiniSpace.Services.Comments/src/MiniSpace.Services.Comments.Infrastructure/Services/CommentService.cs +++ /dev/null @@ -1,58 +0,0 @@ -using MiniSpace.Services.Comments.Application; -using MiniSpace.Services.Comments.Application.Commands; -using MiniSpace.Services.Comments.Application.Dto; -using MiniSpace.Services.Comments.Application.Exceptions; -using MiniSpace.Services.Comments.Application.Services; -using MiniSpace.Services.Comments.Core.Wrappers; -using MiniSpace.Services.Comments.Core.Entities; -using MiniSpace.Services.Comments.Core.Repositories; -using System.Linq; -using System.Threading.Tasks; -using System.Diagnostics.CodeAnalysis; - -namespace MiniSpace.Services.Comments.Infrastructure.Services -{ - [ExcludeFromCodeCoverage] - public class CommentService : ICommentService - { - private readonly ICommentRepository _commentRepository; - - public CommentService(ICommentRepository commentRepository) - { - _commentRepository = commentRepository; - } - - public async Task> BrowseCommentsAsync(SearchComments command) - { - if (!Enum.TryParse(command.CommentContext, true, out var context)) - { - throw new InvalidCommentContextException(command.CommentContext); - } - - var pageNumber = command.Pageable.Page < 1 ? 1 : command.Pageable.Page; - var pageSize = command.Pageable.Size > 10 ? 10 : command.Pageable.Size; - - var request = new BrowseCommentsRequest( - pageNumber, - pageSize, - command.ContextId, - context, - command.ParentId, - command.Pageable.Sort.SortBy, - command.Pageable.Sort.Direction - ); - - var result = await _commentRepository.BrowseCommentsAsync(request); - - var commentDtos = result.Items.Select(c => new CommentDto(c)); - var pagedResponse = new PagedResponse( - commentDtos, - result.Page, - result.PageSize, - result.TotalItems - ); - - return pagedResponse; - } - } -} diff --git a/MiniSpace.Services.Communication/.gitignore b/MiniSpace.Services.Communication/.gitignore new file mode 100644 index 000000000..c619427ad --- /dev/null +++ b/MiniSpace.Services.Communication/.gitignore @@ -0,0 +1,338 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +bin/ +obj/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ + + +# Ignore appsettings.json files +**/appsettings*.json \ No newline at end of file diff --git a/MiniSpace.Services.Communication/Dockerfile b/MiniSpace.Services.Communication/Dockerfile new file mode 100644 index 000000000..88978fc22 --- /dev/null +++ b/MiniSpace.Services.Communication/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY . . + +RUN dotnet publish src/MiniSpace.Services.Communication.Api -c Release -o out + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app + +COPY --from=build /app/out . + +ENV ASPNETCORE_URLS=http://*:80 +ENV ASPNETCORE_ENVIRONMENT=docker +ENV NTRADA_CONFIG=ntrada.docker + +ENTRYPOINT ["dotnet", "MiniSpace.Services.Communication.Api.dll"] \ No newline at end of file diff --git a/MiniSpace.Services.Communication/LICENSE b/MiniSpace.Services.Communication/LICENSE new file mode 100644 index 000000000..c3a0bdfb9 --- /dev/null +++ b/MiniSpace.Services.Communication/LICENSE @@ -0,0 +1,89 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +## TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +### 1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, that is made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +### 2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +### 3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +### 4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright notices to Your version of this material, provided that such additional copyright notices are not considered to be modifications of the License. + +### 5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +### 6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +### 7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +### 8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +### 9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, protection, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +## END OF TERMS AND CONDITIONS + +### Appendix: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be placed in the LICENSE file in the root directory of your source code (if not already present): + + Copyright 2024 distributed_minispace_team + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MiniSpace.Services.Communication/MiniSpace.Services.Communication.sln b/MiniSpace.Services.Communication/MiniSpace.Services.Communication.sln new file mode 100644 index 000000000..ff5faab9b --- /dev/null +++ b/MiniSpace.Services.Communication/MiniSpace.Services.Communication.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0E976732-0CD9-4D2E-B989-998B124073BA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Api", "src\MiniSpace.Services.Students.Api\MiniSpace.Services.Students.Api.csproj", "{D915BFB5-D2D4-44C8-A3A3-379419079F06}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Application", "src\MiniSpace.Services.Students.Application\MiniSpace.Services.Students.Application.csproj", "{97B39658-9B33-4124-90E5-102FDA7D3733}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Core", "src\MiniSpace.Services.Students.Core\MiniSpace.Services.Students.Core.csproj", "{C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.Students.Infrastructure", "src\MiniSpace.Services.Students.Infrastructure\MiniSpace.Services.Students.Infrastructure.csproj", "{B2997BE8-0CE3-45DD-98E9-80599B070C25}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D915BFB5-D2D4-44C8-A3A3-379419079F06}.Release|Any CPU.Build.0 = Release|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97B39658-9B33-4124-90E5-102FDA7D3733}.Release|Any CPU.Build.0 = Release|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16}.Release|Any CPU.Build.0 = Release|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2997BE8-0CE3-45DD-98E9-80599B070C25}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D915BFB5-D2D4-44C8-A3A3-379419079F06} = {0E976732-0CD9-4D2E-B989-998B124073BA} + {97B39658-9B33-4124-90E5-102FDA7D3733} = {0E976732-0CD9-4D2E-B989-998B124073BA} + {C4EFD7FD-9D95-444B-86A4-E4D60E62CD16} = {0E976732-0CD9-4D2E-B989-998B124073BA} + {B2997BE8-0CE3-45DD-98E9-80599B070C25} = {0E976732-0CD9-4D2E-B989-998B124073BA} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Communication/scripts/build.sh b/MiniSpace.Services.Communication/scripts/build.sh new file mode 100644 index 000000000..3affad0eb --- /dev/null +++ b/MiniSpace.Services.Communication/scripts/build.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet build -c release \ No newline at end of file diff --git a/MiniSpace.Services.Communication/scripts/dockerize-tag-push.sh b/MiniSpace.Services.Communication/scripts/dockerize-tag-push.sh new file mode 100755 index 000000000..018a7d1c1 --- /dev/null +++ b/MiniSpace.Services.Communication/scripts/dockerize-tag-push.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +export ASPNETCORE_ENVIRONMENT=docker + +cd .. + +docker build -t minispace.services.communication:latest . + +docker tag minispace.services.communication:latest adrianvsaint/minispace.services.communication:latest + +docker push adrianvsaint/minispace.services.communication:latest diff --git a/MiniSpace.Services.Communication/scripts/start.sh b/MiniSpace.Services.Communication/scripts/start.sh new file mode 100644 index 000000000..824533d3b --- /dev/null +++ b/MiniSpace.Services.Communication/scripts/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export ASPNETCORE_ENVIRONMENT=local +cd ../src/MiniSpace.Services.Communication.Api +dotnet run \ No newline at end of file diff --git a/MiniSpace.Services.Communication/scripts/test.sh b/MiniSpace.Services.Communication/scripts/test.sh new file mode 100644 index 000000000..6046c35a0 --- /dev/null +++ b/MiniSpace.Services.Communication/scripts/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet test \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/.gitignore b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/.gitignore new file mode 100644 index 000000000..94196c0d8 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/.gitignore @@ -0,0 +1,339 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +appsettings.local.json +appsettings.docker.json +appsettings.json + + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +bin/ +obj/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/MiniSpace.Services.Communication.Api.csproj b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/MiniSpace.Services.Communication.Api.csproj new file mode 100644 index 000000000..7219ce31d --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/MiniSpace.Services.Communication.Api.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + latest + MiniSpace.Services.Students.Api + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/MiniSpace.Services.Communication.Api.sln b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/MiniSpace.Services.Communication.Api.sln new file mode 100644 index 000000000..7eb3ebc70 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/MiniSpace.Services.Communication.Api.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Communication.Api", "MiniSpace.Services.Communication.Api.csproj", "{5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DA6DF02-FFC5-4BAB-B1FC-739A9DA14602}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {284B46A7-2ABB-4E20-8B52-4291CC46AE00} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/Program.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/Program.cs new file mode 100644 index 000000000..601b5b58d --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/Program.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Convey; +using Convey.Logging; +using Convey.Types; +using Convey.WebApi; +using Convey.WebApi.CQRS; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.Communication.Application; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Dto; +using MiniSpace.Services.Communication.Application.Queries; +using MiniSpace.Services.Communication.Infrastructure; +using MiniSpace.Services.Communication.Application.Hubs; +using MiniSpace.Services.Communication.Core.Wrappers; +using System; + +namespace MiniSpace.Services.Communication.Api +{ + public class Program + { + public static async Task Main(string[] args) + => await WebHost.CreateDefaultBuilder(args) + .ConfigureServices(services => + { + services.AddConvey() + .AddWebApi() + .AddApplication() + .AddInfrastructure(); + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials() + .SetIsOriginAllowed((host) => true)); + }); + services.AddSignalR(); + services.AddAuthentication(); + services.AddAuthorization(); + }) + .Configure(app => app + .UseInfrastructure() + .UseRouting() + .UseCors("CorsPolicy") + .UseAuthentication() + .UseAuthorization() + .UseEndpoints(endpoints => + { + endpoints.MapHub("/chatHub").RequireCors("CorsPolicy"); + }) + .UseDispatcherEndpoints(endpoints => endpoints + + // Chat-related endpoints + .Get>("communication/chats/user/{userId}") + .Get("communication/chats/{chatId}") + .Get>("communication/chats/{chatId}/messages") + .Post("communication/chats", async (cmd, ctx) => + { + try + { + var chatId = await ctx.RequestServices.GetService() + .CreateChatAsync(cmd.ChatId, cmd.ParticipantIds, cmd.ChatName); + ctx.Response.StatusCode = StatusCodes.Status200OK; + await ctx.Response.WriteJsonAsync(new { ChatId = chatId }); + } + catch (Exception ex) + { + ctx.Response.StatusCode = StatusCodes.Status500InternalServerError; + await ctx.Response.WriteJsonAsync(new { Error = ex.Message }); + } + }) + + .Put("communication/chats/{chatId}/users") + .Delete("communication/chats/{chatId}/{userId}") + + // Message-related endpoints + .Post("communication/chats/{chatId}/messages") + .Put("communication/chats/{chatId}/messages/{messageId}/status") + .Delete("communication/chats/{chatId}/messages/{messageId}") + )) + .UseLogging() + .UseLogging() + .Build() + .RunAsync(); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/Properties/launchSettings.json b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/Properties/launchSettings.json new file mode 100644 index 000000000..41c0e71a3 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Api/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5016" + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + }, + "MiniSpace.Services.Communication": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:5016", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/.gitignore b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/.gitignore new file mode 100644 index 000000000..64def56a6 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/.gitignore @@ -0,0 +1,331 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/AddUserToChat.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/AddUserToChat.cs new file mode 100644 index 000000000..b9c8b36bf --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/AddUserToChat.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Commands; +using System; + +namespace MiniSpace.Services.Communication.Application.Commands +{ + public class AddUserToChat : ICommand + { + public Guid ChatId { get; } + public Guid UserId { get; } + + public AddUserToChat(Guid chatId, Guid userId) + { + ChatId = chatId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/CreateChat.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/CreateChat.cs new file mode 100644 index 000000000..8a81dfd02 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/CreateChat.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Commands; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Application.Commands +{ + public class CreateChat : ICommand + { + public Guid ChatId { get; } + public List ParticipantIds { get; } + public string ChatName { get; } + + public CreateChat(Guid chatId, List participantIds, string chatName = null) + { + ChatId = chatId; + ParticipantIds = participantIds ?? new List(); + ChatName = chatName; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/DeleteChat.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/DeleteChat.cs new file mode 100644 index 000000000..f1cf27b57 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/DeleteChat.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Commands; +using Microsoft.AspNetCore.Mvc; +using System; + +namespace MiniSpace.Services.Communication.Application.Commands +{ + public class DeleteChat : ICommand + { + public Guid ChatId { get; set; } + + public Guid UserId { get; set; } + + public DeleteChat(Guid chatId, Guid userId) + { + ChatId = chatId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/DeleteMessage.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/DeleteMessage.cs new file mode 100644 index 000000000..b6fc99d17 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/DeleteMessage.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Commands; +using Microsoft.AspNetCore.Mvc; +using System; + +namespace MiniSpace.Services.Communication.Application.Commands +{ + public class DeleteMessage : ICommand + { + [FromRoute] + public Guid MessageId { get; } + + [FromQuery] + public Guid ChatId { get; } + + public DeleteMessage(Guid messageId, Guid chatId) + { + MessageId = messageId; + ChatId = chatId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/AddUserToChatHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/AddUserToChatHandler.cs new file mode 100644 index 000000000..9a0bbd1be --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/AddUserToChatHandler.cs @@ -0,0 +1,38 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Events; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Core.Repositories; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Application.Commands.Handlers +{ + public class AddUserToChatHandler : ICommandHandler + { + private readonly IUserChatsRepository _userChatsRepository; + private readonly IMessageBroker _messageBroker; + + public AddUserToChatHandler(IUserChatsRepository userChatsRepository, IMessageBroker messageBroker) + { + _userChatsRepository = userChatsRepository; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(AddUserToChat command, CancellationToken cancellationToken) + { + var userChats = await _userChatsRepository.GetByUserIdAsync(command.UserId) ?? new UserChats(command.UserId); + var chat = userChats.GetChatById(command.ChatId); + + if (chat == null) + { + chat = new Chat(new List { command.UserId }); + userChats.AddChat(chat); + await _userChatsRepository.AddOrUpdateAsync(userChats); + } + + await _messageBroker.PublishAsync(new UserAddedToChat(command.ChatId, command.UserId)); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/CreateChatHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/CreateChatHandler.cs new file mode 100644 index 000000000..121c5316a --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/CreateChatHandler.cs @@ -0,0 +1,31 @@ +using Convey.CQRS.Commands; +using Microsoft.Extensions.Logging; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Services; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Application.Commands.Handlers +{ + public class CreateChatHandler : ICommandHandler + { + private readonly ICommunicationService _communicationService; + private readonly ILogger _logger; + + public CreateChatHandler(ICommunicationService communicationService, ILogger logger) + { + _communicationService = communicationService; + _logger = logger; + } + + public async Task HandleAsync(CreateChat command, CancellationToken cancellationToken) + { + _logger.LogInformation($"Handling CreateChat command for Chat ID: {command.ChatId}"); + + // Call the CommunicationService to create the chat + var chatId = await _communicationService.CreateChatAsync(command.ChatId, command.ParticipantIds, command.ChatName); + + _logger.LogInformation($"Chat created with ID: {chatId}"); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/DeleteChatHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/DeleteChatHandler.cs new file mode 100644 index 000000000..3622fc5f5 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/DeleteChatHandler.cs @@ -0,0 +1,31 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Events; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Core.Repositories; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Application.Commands.Handlers +{ + public class DeleteChatHandler : ICommandHandler + { + private readonly IUserChatsRepository _userChatsRepository; + private readonly IMessageBroker _messageBroker; + + public DeleteChatHandler(IUserChatsRepository userChatsRepository, IMessageBroker messageBroker) + { + _userChatsRepository = userChatsRepository; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(DeleteChat command, CancellationToken cancellationToken) + { + Console.WriteLine($"Received DeleteChat command - ChatId: {command.ChatId}, UserId: {command.UserId}"); + + await _userChatsRepository.DeleteChatAsync(command.UserId, command.ChatId); + + await _messageBroker.PublishAsync(new ChatDeleted(command.ChatId)); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/DeleteMessageHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/DeleteMessageHandler.cs new file mode 100644 index 000000000..daaf6af7b --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/DeleteMessageHandler.cs @@ -0,0 +1,40 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Core.Repositories; +using MiniSpace.Services.Communication.Application.Events; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Application.Commands.Handlers +{ + public class DeleteMessageHandler : ICommandHandler + { + private readonly IUserChatsRepository _userChatsRepository; + private readonly IMessageBroker _messageBroker; + + public DeleteMessageHandler(IUserChatsRepository userChatsRepository, IMessageBroker messageBroker) + { + _userChatsRepository = userChatsRepository; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(DeleteMessage command, CancellationToken cancellationToken) + { + var userChats = await _userChatsRepository.GetByUserIdAsync(command.ChatId); + var chat = userChats?.GetChatById(command.ChatId); + + if (chat != null) + { + var message = chat.Messages.Find(m => m.Id == command.MessageId); + if (message != null) + { + chat.Messages.Remove(message); + await _userChatsRepository.UpdateAsync(userChats); + + await _messageBroker.PublishAsync(new MessageStatusUpdated(command.ChatId, command.MessageId, "Deleted")); + } + } + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/RemoveUserFromChatHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/RemoveUserFromChatHandler.cs new file mode 100644 index 000000000..5a7291c36 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/RemoveUserFromChatHandler.cs @@ -0,0 +1,36 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Events; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Core.Repositories; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Application.Commands.Handlers +{ + public class RemoveUserFromChatHandler : ICommandHandler + { + private readonly IUserChatsRepository _userChatsRepository; + private readonly IMessageBroker _messageBroker; + + public RemoveUserFromChatHandler(IUserChatsRepository userChatsRepository, IMessageBroker messageBroker) + { + _userChatsRepository = userChatsRepository; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(RemoveUserFromChat command, CancellationToken cancellationToken) + { + var userChats = await _userChatsRepository.GetByUserIdAsync(command.UserId); + var chat = userChats?.GetChatById(command.ChatId); + + if (chat != null) + { + chat.ParticipantIds.Remove(command.UserId); + await _userChatsRepository.UpdateAsync(userChats); + + await _messageBroker.PublishAsync(new UserAddedToChat(command.ChatId, command.UserId)); + } + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/SendMessageHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/SendMessageHandler.cs new file mode 100644 index 000000000..c273375f6 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/SendMessageHandler.cs @@ -0,0 +1,152 @@ +using Convey.CQRS.Commands; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.SignalR; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Events; +using MiniSpace.Services.Communication.Application.Hubs; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Core.Repositories; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System; +using MiniSpace.Services.Communication.Application.Dto; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Application.Commands.Handlers +{ + public class SendMessageHandler : ICommandHandler + { + private readonly IUserChatsRepository _userChatsRepository; + private readonly IMessageBroker _messageBroker; + private readonly ILogger _logger; + private readonly IHubContext _hubContext; + + public SendMessageHandler( + IUserChatsRepository userChatsRepository, + IMessageBroker messageBroker, + ILogger logger, + IHubContext hubContext) + { + _userChatsRepository = userChatsRepository; + _messageBroker = messageBroker; + _logger = logger; + _hubContext = hubContext; + } + + public async Task HandleAsync(SendMessage command, CancellationToken cancellationToken) + { + // Retrieve the chat from the sender's chat list + var senderChats = await _userChatsRepository.GetByUserIdAsync(command.SenderId); + var chat = senderChats?.GetChatById(command.ChatId); + + if (chat == null) + { + _logger.LogWarning($"Chat with id {command.ChatId} not found for user with id {command.SenderId}. Checking if it exists for other participants..."); + + // If the chat is not found, check if it exists for any other participant + chat = await FindChatInAllParticipants(command.SenderId, command.ChatId); + + if (chat == null) + { + _logger.LogInformation($"No existing chat found. Creating a new chat for the participants."); + // If no chat is found at all, create a new one + // If creating a new chat + chat = new Chat(command.ChatId, new List { command.SenderId }, new List()); + + await CreateOrUpdateChatForAllParticipants(chat); + } + else + { + // Restore the chat for the sender + senderChats ??= new UserChats(command.SenderId); + senderChats.AddChat(chat); + await _userChatsRepository.AddOrUpdateAsync(senderChats); + } + } + + // Create the message to be sent + var message = new Message( + chatId: chat.Id, + senderId: command.SenderId, + receiverId: Guid.Empty, // Unused in this context + content: command.Content, + type: Enum.Parse(command.MessageType) + ); + + _logger.LogInformation($"Sending message with id {message.Id} to chat with id {chat.Id}"); + + chat.AddMessage(message); + + // Save the updated chat and message to the database + await UpdateChatForAllParticipants(chat); + + // Use the message ID from the database for the MessageDto + var messageDto = new MessageDto + { + Id = message.Id, // Use the ID from the database + ChatId = chat.Id, + SenderId = command.SenderId, + Content = command.Content, + Timestamp = message.Timestamp // Assuming Message has a Timestamp property + }; + + + // Notify the participant via SignalR + await ChatHub.SendMessageToUser(_hubContext, command.SenderId.ToString(), messageDto, _logger); + + // Publish the MessageSent event for further processing + await _messageBroker.PublishAsync(new MessageSent( + chatId: chat.Id, + messageId: message.Id, + senderId: command.SenderId, + content: command.Content + )); + } + + private async Task FindChatInAllParticipants(Guid senderId, Guid chatId) + { + // Go through all participant chats to find if the chat already exists + var participantIds = await _userChatsRepository.GetParticipantIdsByChatIdAsync(chatId); + + foreach (var participantId in participantIds) + { + if (participantId == senderId) continue; + + var participantChats = await _userChatsRepository.GetByUserIdAsync(participantId); + var existingChat = participantChats?.GetChatById(chatId); + + if (existingChat != null) + { + _logger.LogInformation($"Chat found for another participant with id {participantId}. Restoring chat for sender."); + return existingChat; + } + } + + return null; + } + + private async Task CreateOrUpdateChatForAllParticipants(Chat chat) + { + foreach (var participantId in chat.ParticipantIds) + { + var participantChats = await _userChatsRepository.GetByUserIdAsync(participantId) ?? new UserChats(participantId); + participantChats.AddChat(chat); + await _userChatsRepository.AddOrUpdateAsync(participantChats); + } + } + + private async Task UpdateChatForAllParticipants(Chat chat) + { + foreach (var participantId in chat.ParticipantIds) + { + var participantChats = await _userChatsRepository.GetByUserIdAsync(participantId) ?? new UserChats(participantId); + var participantChat = participantChats.GetChatById(chat.Id) ?? chat; + participantChat.AddMessage(chat.Messages.Last()); + await _userChatsRepository.AddOrUpdateAsync(participantChats); + } + } + + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/UpdateMessageStatusHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/UpdateMessageStatusHandler.cs new file mode 100644 index 000000000..c18402d17 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/Handlers/UpdateMessageStatusHandler.cs @@ -0,0 +1,36 @@ +using Convey.CQRS.Commands; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.SignalR; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Events; +using MiniSpace.Services.Communication.Application.Hubs; +using MiniSpace.Services.Communication.Application.Services; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Application.Commands.Handlers +{ + public class UpdateMessageStatusHandler : ICommandHandler + { + private readonly ICommunicationService _communicationService; + private readonly IHubContext _hubContext; + private readonly ILogger _logger; + + public UpdateMessageStatusHandler( + ICommunicationService communicationService, + IHubContext hubContext, + ILogger logger) + { + _communicationService = communicationService; + _hubContext = hubContext; + _logger = logger; + } + + public async Task HandleAsync(UpdateMessageStatus command, CancellationToken cancellationToken) + { + await _communicationService.UpdateMessageStatusAsync(command.ChatId, command.MessageId, command.Status); + _logger.LogInformation($"Message status updated: ChatId={command.ChatId}, MessageId={command.MessageId}, Status={command.Status}"); + await ChatHub.SendMessageStatusUpdate(_hubContext, command.ChatId.ToString(), command.MessageId, command.Status, _logger); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/RemoveUserFromChat.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/RemoveUserFromChat.cs new file mode 100644 index 000000000..53de89eaa --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/RemoveUserFromChat.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Commands; +using System; + +namespace MiniSpace.Services.Communication.Application.Commands +{ + public class RemoveUserFromChat : ICommand + { + public Guid ChatId { get; } + public Guid UserId { get; } + + public RemoveUserFromChat(Guid chatId, Guid userId) + { + ChatId = chatId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/SendMessage.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/SendMessage.cs new file mode 100644 index 000000000..a5df08bbb --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/SendMessage.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Commands; +using System; + +namespace MiniSpace.Services.Communication.Application.Commands +{ + public class SendMessage : ICommand + { + public Guid ChatId { get; } + public Guid SenderId { get; } + public string Content { get; } + public string MessageType { get; } + + public SendMessage(Guid chatId, Guid senderId, string content, string messageType = "Text") + { + ChatId = chatId; + SenderId = senderId; + Content = content; + MessageType = messageType; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/UpdateMessageStatus.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/UpdateMessageStatus.cs new file mode 100644 index 000000000..cf809704e --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Commands/UpdateMessageStatus.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Commands; +using System; + +namespace MiniSpace.Services.Communication.Application.Commands +{ + public class UpdateMessageStatus : ICommand + { + public Guid ChatId { get; set; } + public Guid MessageId { get; set; } + public string Status { get; set; } + + public UpdateMessageStatus(Guid chatId, Guid messageId, string status) + { + ChatId = chatId; + MessageId = messageId; + Status = status; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/ContractAttribute.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/ContractAttribute.cs new file mode 100644 index 000000000..19f02dfbf --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/ContractAttribute.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Communication.Application +{ + public class ContractAttribute : Attribute + { + + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/ChatDto.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/ChatDto.cs new file mode 100644 index 000000000..3b7c3224e --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/ChatDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Application.Dto +{ + public class ChatDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public List ParticipantIds { get; set; } + public List Messages { get; set; } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/MessageDto.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/MessageDto.cs new file mode 100644 index 000000000..d4970dd25 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/MessageDto.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Communication.Application.Dto +{ + public class MessageDto + { + public Guid Id { get; set; } + public Guid ChatId { get; set; } + public Guid SenderId { get; set; } + public string Content { get; set; } + public DateTime Timestamp { get; set; } + public string MessageType { get; set; } + public string Status { get; set; } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/UserChatDto.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/UserChatDto.cs new file mode 100644 index 000000000..f2bc7eb2f --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/UserChatDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Application.Dto +{ + public class UserChatDto + { + public Guid UserId { get; set; } + public List Chats { get; set; } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/UserDto.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/UserDto.cs new file mode 100644 index 000000000..80587cbcf --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Dto/UserDto.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + + +namespace MiniSpace.Services.Communication.Application.Dto +{ + [ExcludeFromCodeCoverage] + public class UserDto + { + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string ProfileImageUrl { get; set; } + public string Description { get; set; } + public DateTime? DateOfBirth { get; set; } + public bool EmailNotifications { get; set; } + public bool IsBanned { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public string ContactEmail { get; set; } + public string BannerUrl { get; set; } + public string PhoneNumber { get; set; } + public IEnumerable Languages { get; set; } + public IEnumerable Interests { get; set; } + + // public IEnumerable Education { get; set; } + // public IEnumerable Work { get; set; } + public bool IsTwoFactorEnabled { get; set; } + public string TwoFactorSecret { get; set; } + public IEnumerable InterestedInEvents { get; set; } + public IEnumerable SignedUpEvents { get; set; } + public string Country { get; set; } + public string City { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/ChatCreated.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/ChatCreated.cs new file mode 100644 index 000000000..736768c16 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/ChatCreated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Application.Events +{ + public class ChatCreated : IEvent + { + public Guid ChatId { get; set; } + public List ParticipantIds { get; set; } + + public ChatCreated(Guid chatId, List participantIds) + { + ChatId = chatId; + ParticipantIds = participantIds; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/ChatDeleted.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/ChatDeleted.cs new file mode 100644 index 000000000..6c06f258a --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/ChatDeleted.cs @@ -0,0 +1,15 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events +{ + public class ChatDeleted : IEvent + { + public Guid ChatId { get; } + + public ChatDeleted(Guid chatId) + { + ChatId = chatId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/MessageSent.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/MessageSent.cs new file mode 100644 index 000000000..8d0d17727 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/MessageSent.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events +{ + public class MessageSent : IEvent + { + public Guid ChatId { get; } + public Guid MessageId { get; } + public Guid SenderId { get; } + public string Content { get; } + + public MessageSent(Guid chatId, Guid messageId, Guid senderId, string content) + { + ChatId = chatId; + MessageId = messageId; + SenderId = senderId; + Content = content; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/MessageStatusUpdated.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/MessageStatusUpdated.cs new file mode 100644 index 000000000..359ed6415 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/MessageStatusUpdated.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events +{ + public class MessageStatusUpdated : IEvent + { + public Guid ChatId { get; set; } + public Guid MessageId { get; set; } + public string Status { get; set; } + + public MessageStatusUpdated(Guid chatId, Guid messageId, string status) + { + ChatId = chatId; + MessageId = messageId; + Status = status; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatCreationRejected.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatCreationRejected.cs new file mode 100644 index 000000000..063316929 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatCreationRejected.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events.Rejected +{ + public class ChatCreationRejected : IRejectedEvent + { + public Guid ChatId { get; } + public string Reason { get; } + public string Code { get; } + + public ChatCreationRejected(Guid chatId, string reason, string code) + { + ChatId = chatId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatDeletionRejected.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatDeletionRejected.cs new file mode 100644 index 000000000..7ea9a517a --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatDeletionRejected.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events.Rejected +{ + public class ChatDeletionRejected : IRejectedEvent + { + public Guid ChatId { get; } + public string Reason { get; } + public string Code { get; } + + public ChatDeletionRejected(Guid chatId, string reason, string code) + { + ChatId = chatId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatProcessRejected.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatProcessRejected.cs new file mode 100644 index 000000000..51363312c --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/ChatProcessRejected.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events.Rejected +{ + public class ChatProcessRejected : IRejectedEvent + { + public Guid ChatId { get; } + public string Reason { get; } + public string Code { get; } + + public ChatProcessRejected(Guid chatId, string reason, string code) + { + ChatId = chatId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/MessageProcessRejected.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/MessageProcessRejected.cs new file mode 100644 index 000000000..334a1f1f5 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/MessageProcessRejected.cs @@ -0,0 +1,19 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events.Rejected +{ + public class MessageProcessRejected : IRejectedEvent + { + public Guid MessageId { get; } + public string Reason { get; } + public string Code { get; } + + public MessageProcessRejected(Guid messageId, string reason, string code) + { + MessageId = messageId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/MessageSendRejected.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/MessageSendRejected.cs new file mode 100644 index 000000000..61629d6a5 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/MessageSendRejected.cs @@ -0,0 +1,22 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events.Rejected +{ + public class MessageSendRejected : IRejectedEvent + { + public Guid ChatId { get; } + public Guid MessageId { get; } + public string Reason { get; } + public string Code { get; } + + public MessageSendRejected(Guid chatId, Guid messageId, string reason, string code) + { + ChatId = chatId; + MessageId = messageId; + Reason = reason; + Code = code; + } + } + +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/UserAdditionToChatRejected.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/UserAdditionToChatRejected.cs new file mode 100644 index 000000000..5589a4c38 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/Rejected/UserAdditionToChatRejected.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events.Rejected +{ + public class UserAdditionToChatRejected : IRejectedEvent + { + public Guid ChatId { get; } + public Guid UserId { get; } + public string Reason { get; } + public string Code { get; } + + public UserAdditionToChatRejected(Guid chatId, Guid userId, string reason, string code) + { + ChatId = chatId; + UserId = userId; + Reason = reason; + Code = code; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/UserAddedToChat.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/UserAddedToChat.cs new file mode 100644 index 000000000..84c8c1de1 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Events/UserAddedToChat.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Communication.Application.Events +{ + public class UserAddedToChat : IEvent + { + public Guid ChatId { get; } + public Guid UserId { get; } + + public UserAddedToChat(Guid chatId, Guid userId) + { + ChatId = chatId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/AppException.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/AppException.cs new file mode 100644 index 000000000..78c42cdd9 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/AppException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Communication.Application.Exceptions +{ + public class AppException : Exception + { + public virtual string Code { get; } + + protected AppException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/ChatNotFoundException.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/ChatNotFoundException.cs new file mode 100644 index 000000000..af906f1f9 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/ChatNotFoundException.cs @@ -0,0 +1,17 @@ +using System; + +namespace MiniSpace.Services.Communication.Application.Exceptions +{ + public class ChatNotFoundException : AppException + { + public override string Code { get; } = "chat_not_found"; + + public Guid ChatId { get; } + + public ChatNotFoundException(Guid chatId) + : base($"Chat with ID '{chatId}' was not found.") + { + ChatId = chatId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/InvalidChatOperationException.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/InvalidChatOperationException.cs new file mode 100644 index 000000000..ff06611ad --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/InvalidChatOperationException.cs @@ -0,0 +1,14 @@ +using System; + +namespace MiniSpace.Services.Communication.Application.Exceptions +{ + public class InvalidChatOperationException : AppException + { + public override string Code { get; } = "invalid_chat_operation"; + + public InvalidChatOperationException(string message) + : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/MessageNotFoundException.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/MessageNotFoundException.cs new file mode 100644 index 000000000..6b384f680 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Exceptions/MessageNotFoundException.cs @@ -0,0 +1,17 @@ +using System; + +namespace MiniSpace.Services.Communication.Application.Exceptions +{ + public class MessageNotFoundException : AppException + { + public override string Code { get; } = "message_not_found"; + + public Guid MessageId { get; } + + public MessageNotFoundException(Guid messageId) + : base($"Message with ID '{messageId}' was not found.") + { + MessageId = messageId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Extensions.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Extensions.cs new file mode 100644 index 000000000..d03c02a20 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Extensions.cs @@ -0,0 +1,16 @@ +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Communication.Application +{ + public static class Extensions + { + public static IConveyBuilder AddApplication(this IConveyBuilder builder) + => builder + .AddCommandHandlers() + .AddEventHandlers() + .AddInMemoryCommandDispatcher() + .AddInMemoryEventDispatcher(); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Hubs/ChatHub.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Hubs/ChatHub.cs new file mode 100644 index 000000000..d570aec26 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Hubs/ChatHub.cs @@ -0,0 +1,108 @@ +using Microsoft.AspNetCore.SignalR; +using MiniSpace.Services.Communication.Application.Dto; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace MiniSpace.Services.Communication.Application.Hubs +{ + public class ChatHub : Hub + { + private readonly ILogger _logger; + + public ChatHub(ILogger logger) + { + _logger = logger; + } + + public override async Task OnConnectedAsync() + { + var userId = Context.GetHttpContext().Request.Query["userId"]; + if (!string.IsNullOrEmpty(userId)) + { + await Groups.AddToGroupAsync(Context.ConnectionId, userId); + _logger.LogInformation($"User {userId} connected and added to group with Connection ID: {Context.ConnectionId}"); + } + else + { + _logger.LogWarning("User ID is missing in the query string."); + } + await base.OnConnectedAsync(); + } + + public override async Task OnDisconnectedAsync(Exception exception) + { + var userId = Context.GetHttpContext().Request.Query["userId"]; + if (!string.IsNullOrEmpty(userId)) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId); + _logger.LogInformation($"User {userId} disconnected and removed from group with Connection ID: {Context.ConnectionId}"); + } + await base.OnDisconnectedAsync(exception); + } + + public async Task SendMessage(string userId, MessageDto message) + { + var jsonMessage = JsonSerializer.Serialize(message); + _logger.LogInformation($"Sending message to user {userId}: {jsonMessage}"); + await Clients.User(userId).SendAsync("ReceiveMessage", jsonMessage); + } + + public async Task SendMessageToGroup(string groupName, MessageDto message) + { + var jsonMessage = JsonSerializer.Serialize(message); + _logger.LogInformation($"Sending message to group {groupName}: {jsonMessage}"); + await Clients.Group(groupName).SendAsync("ReceiveGroupMessage", jsonMessage); + } + + public async Task AddToGroup(string groupName) + { + _logger.LogInformation($"Adding connection {Context.ConnectionId} to group {groupName}"); + await Groups.AddToGroupAsync(Context.ConnectionId, groupName); + } + + public async Task RemoveFromGroup(string groupName) + { + _logger.LogInformation($"Removing connection {Context.ConnectionId} from group {groupName}"); + await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); + } + + public static async Task SendMessageToUser(IHubContext hubContext, string userId, MessageDto message, ILogger logger) + { + var jsonMessage = JsonSerializer.Serialize(message); + logger.LogInformation($"Sending message to user {userId}: {jsonMessage}"); + await hubContext.Clients.All.SendAsync("ReceiveMessage", jsonMessage); + } + + public static async Task BroadcastMessage(IHubContext hubContext, MessageDto message, ILogger logger) + { + var jsonMessage = JsonSerializer.Serialize(message); + logger.LogInformation($"Broadcasting message to all users: {jsonMessage}"); + await hubContext.Clients.All.SendAsync("ReceiveMessage", jsonMessage); + } + + + public static async Task SendMessageStatusUpdate(IHubContext hubContext, string chatId, Guid messageId, string status, ILogger logger) + { + var statusUpdate = new + { + ChatId = chatId, + MessageId = messageId, + Status = status + }; + + logger.LogInformation($"Sending message status update to chat {chatId} for message {messageId} with status {status}"); + + var jsonStatusUpdate = JsonSerializer.Serialize(statusUpdate); + + await hubContext.Clients.All.SendAsync("ReceiveMessageStatusUpdate", jsonStatusUpdate); + } + + public async Task SendTypingNotification(string chatId, string userId, bool isTyping) + { + _logger.LogInformation($"User {userId} is typing in chat {chatId}: {isTyping}"); + await Clients.All.SendAsync("ReceiveTypingNotification", userId, isTyping); + } + + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/IAppContext.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/IAppContext.cs new file mode 100644 index 000000000..c4b6a6756 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/IAppContext.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.Communication.Application +{ + public interface IAppContext + { + string RequestId { get; } + IIdentityContext Identity { get; } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/IIdentityContext.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/IIdentityContext.cs new file mode 100644 index 000000000..50c867b07 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/IIdentityContext.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Communication.Application +{ + public interface IIdentityContext + { + Guid Id { get; } + string Role { get; } + string Name { get; } + string Email { get; } + bool IsAuthenticated { get; } + bool IsAdmin { get; } + bool IsBanned { get; } + IDictionary Claims { get; } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/MiniSpace.Services.Communication.Application.csproj b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/MiniSpace.Services.Communication.Application.csproj new file mode 100644 index 000000000..021af6a32 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/MiniSpace.Services.Communication.Application.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/MiniSpace.Services.Communication.Application.sln b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/MiniSpace.Services.Communication.Application.sln new file mode 100644 index 000000000..88ccceadd --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/MiniSpace.Services.Communication.Application.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Communication.Application", "MiniSpace.Services.Communication.Application.csproj", "{85F8B10B-A401-4C70-9202-F2A8C1C7AF61}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {85F8B10B-A401-4C70-9202-F2A8C1C7AF61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85F8B10B-A401-4C70-9202-F2A8C1C7AF61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85F8B10B-A401-4C70-9202-F2A8C1C7AF61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85F8B10B-A401-4C70-9202-F2A8C1C7AF61}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AE2AFDD2-927B-4122-835F-837E29C3330F} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetChatById.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetChatById.cs new file mode 100644 index 000000000..db0a551a5 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetChatById.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Communication.Application.Dto; +using System; + +namespace MiniSpace.Services.Communication.Application.Queries +{ + public class GetChatById : IQuery + { + public Guid ChatId { get; } + + public GetChatById(Guid chatId) + { + ChatId = chatId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetMessagesForChat.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetMessagesForChat.cs new file mode 100644 index 000000000..fb7d10508 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetMessagesForChat.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Communication.Application.Dto; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Application.Queries +{ + public class GetMessagesForChat : IQuery> + { + public Guid ChatId { get; } + + public GetMessagesForChat(Guid chatId) + { + ChatId = chatId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetUserChats.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetUserChats.cs new file mode 100644 index 000000000..1728621e0 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Queries/GetUserChats.cs @@ -0,0 +1,21 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Communication.Application.Dto; +using System; +using MiniSpace.Services.Communication.Core.Wrappers; + +namespace MiniSpace.Services.Communication.Application.Queries +{ + public class GetUserChats : IQuery> + { + public Guid UserId { get; } + public int Page { get; } + public int PageSize { get; } + + public GetUserChats(Guid userId, int page, int pageSize) + { + UserId = userId; + Page = page > 0 ? page : 1; + PageSize = pageSize > 0 ? pageSize : 10; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/Clients/IStudentsServiceClient.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/Clients/IStudentsServiceClient.cs new file mode 100644 index 000000000..8dd50b92f --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/Clients/IStudentsServiceClient.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using MiniSpace.Services.Communication.Application.Dto; + +namespace MiniSpace.Services.Communication.Application.Services.Clients +{ + public interface IStudentsServiceClient + { + Task GetAsync(Guid id); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/CommunicationService.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/CommunicationService.cs new file mode 100644 index 000000000..96f67ad1d --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/CommunicationService.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Events; +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Core.Repositories; + +namespace MiniSpace.Services.Communication.Application.Services +{ + public class CommunicationService : ICommunicationService + { + private readonly IUserChatsRepository _userChatsRepository; + private readonly IMessageBroker _messageBroker; + private readonly ILogger _logger; + + public CommunicationService(IUserChatsRepository userChatsRepository, IMessageBroker messageBroker, ILogger logger) + { + _userChatsRepository = userChatsRepository; + _messageBroker = messageBroker; + _logger = logger; + } + + public async Task CreateChatAsync(Guid chatId, List participantIds, string chatName = null) + { + _logger.LogInformation($"Creating chat with ID: {chatId}"); + + // Check if the chat already exists + Chat existingChat = null; + foreach (var participantId in participantIds) + { + var userChats = await _userChatsRepository.GetByUserIdAsync(participantId); + if (userChats != null) + { + existingChat = userChats.Chats.Find(chat => + chat.ParticipantIds.Count == participantIds.Count && + chat.ParticipantIds.All(pid => participantIds.Contains(pid)) + ); + + if (existingChat != null) + { + _logger.LogWarning($"Chat with ID: {existingChat.Id} already exists. Returning existing chat ID."); + return existingChat.Id; + } + } + } + + // Create a new chat if none exists + var newChat = new Chat(chatId, participantIds, new List()); + foreach (var participantId in participantIds) + { + var userChats = await _userChatsRepository.GetByUserIdAsync(participantId) ?? new UserChats(participantId); + userChats.AddChat(newChat); + await _userChatsRepository.AddOrUpdateAsync(userChats); + } + + await _messageBroker.PublishAsync(new ChatCreated(newChat.Id, participantIds)); + + _logger.LogInformation($"New chat created with ID: {newChat.Id}"); + return newChat.Id; + } + + + public async Task UpdateMessageStatusAsync(Guid chatId, Guid messageId, string status) + { + // Retrieve the chat using the GetByChatIdAsync method + var chat = await _userChatsRepository.GetByChatIdAsync(chatId); + if (chat == null) + { + _logger.LogWarning($"Chat with ID {chatId} not found."); + return; + } + + var message = chat.Messages.FirstOrDefault(m => m.Id == messageId); + if (message == null) + { + _logger.LogWarning($"Message with ID {messageId} not found in chat with ID {chatId}."); + return; + } + + switch (status) + { + case "Read": + message.MarkAsRead(); + break; + case "Unread": + message.MarkAsUnread(); + break; + case "Deleted": + message.MarkAsDeleted(); + break; + default: + _logger.LogWarning($"Unsupported status '{status}' for message ID {messageId} in chat ID {chatId}."); + return; + } + + foreach (var participantId in chat.ParticipantIds) + { + var userChats = await _userChatsRepository.GetByUserIdAsync(participantId); + if (userChats != null) + { + var existingChat = userChats.GetChatById(chatId); + if (existingChat != null) + { + var messageToUpdate = existingChat.Messages.FirstOrDefault(m => m.Id == messageId); + if (messageToUpdate != null) + { + _logger.LogInformation($"Updating message status for chat ID {chatId} and message ID {messageId} to {status}"); + messageToUpdate.MarkAsRead(); + } + _logger.LogInformation($"Updating chat for participant {participantId} with chat ID {chatId}"); + await _userChatsRepository.UpdateAsync(userChats); + } + } + } + + _logger.LogInformation($"Message status updated: ChatId={chatId}, MessageId={messageId}, Status={status}"); + + await _messageBroker.PublishAsync(new MessageStatusUpdated(chatId, messageId, status)); + } + + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/ICommunicationService.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/ICommunicationService.cs new file mode 100644 index 000000000..da0dd02cd --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/ICommunicationService.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Dto; + +namespace MiniSpace.Services.Communication.Application.Services +{ + public interface ICommunicationService + { + Task CreateChatAsync(Guid chatId, List participantIds, string chatName = null); + Task UpdateMessageStatusAsync(Guid chatId, Guid messageId, string status); + // Task> GetUserChatsAsync(Guid userId); + // Task GetChatByIdAsync(Guid chatId); + // Task SendMessageAsync(SendMessage command); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IDateTimeProvider.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IDateTimeProvider.cs new file mode 100644 index 000000000..527fa2c7e --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IDateTimeProvider.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Communication.Application.Services +{ + public interface IDateTimeProvider + { + DateTime Now { get; } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IEventMapper.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IEventMapper.cs new file mode 100644 index 000000000..0cde987ee --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IEventMapper.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Communication.Core.Events; + +namespace MiniSpace.Services.Communication.Application.Services +{ + public interface IEventMapper + { + IEvent Map(IDomainEvent @event); + IEnumerable MapAll(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IMessageBroker.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IMessageBroker.cs new file mode 100644 index 000000000..405bad2e8 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/IMessageBroker.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.Communication.Application.Services +{ + public interface IMessageBroker + { + Task PublishAsync(params IEvent[] events); + Task PublishAsync(IEnumerable events); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/ISignalRConnectionManager.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/ISignalRConnectionManager.cs new file mode 100644 index 000000000..1df25a459 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Application/Services/ISignalRConnectionManager.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Application.Services +{ + public interface ISignalRConnectionManager + { + Task SendMessageAsync(string user, string message); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/.gitignore b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/.gitignore new file mode 100644 index 000000000..1f7e99963 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/.gitignore @@ -0,0 +1,334 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +bin/ +obj/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/AggregateId.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/AggregateId.cs new file mode 100644 index 000000000..63fe3517a --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/AggregateId.cs @@ -0,0 +1,50 @@ +using MiniSpace.Services.Communication.Core.Exceptions; + +namespace MiniSpace.Services.Communication.Core.Entities +{ + public class AggregateId : IEquatable + { + public Guid Value { get; } + + public AggregateId() + { + Value = Guid.NewGuid(); + } + + public AggregateId(Guid value) + { + if (value == Guid.Empty) + { + throw new InvalidAggregateIdException(); + } + + Value = value; + } + + public bool Equals(AggregateId other) + { + if (ReferenceEquals(null, other)) return false; + return ReferenceEquals(this, other) || Value.Equals(other.Value); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((AggregateId) obj); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public static implicit operator Guid(AggregateId id) + => id.Value; + + public static implicit operator AggregateId(Guid id) + => new AggregateId(id); + + public override string ToString() => Value.ToString(); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/AggregateRoot.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/AggregateRoot.cs new file mode 100644 index 000000000..7d5382101 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/AggregateRoot.cs @@ -0,0 +1,19 @@ +using MiniSpace.Services.Communication.Core.Events; + +namespace MiniSpace.Services.Communication.Core.Entities +{ + public abstract class AggregateRoot + { + private readonly List _events = new List(); + public IEnumerable Events => _events; + public AggregateId Id { get; protected set; } + public int Version { get; protected set; } + + protected void AddEvent(IDomainEvent @event) + { + _events.Add(@event); + } + + public void ClearEvents() => _events.Clear(); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/Chat.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/Chat.cs new file mode 100644 index 000000000..2624138a9 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/Chat.cs @@ -0,0 +1,31 @@ +namespace MiniSpace.Services.Communication.Core.Entities +{ + public class Chat : AggregateRoot + { + public Guid Id { get; private set; } + public List ParticipantIds { get; private set; } + public List Messages { get; private set; } + + // Constructor for creating a new chat with a new Id + public Chat(List participantIds) + { + Id = Guid.NewGuid(); + ParticipantIds = participantIds; + Messages = new List(); + } + + // Constructor for loading an existing chat from the database + public Chat(Guid id, List participantIds, List messages) + { + Id = id; + ParticipantIds = participantIds; + Messages = messages; + } + + public void AddMessage(Message message) + { + Messages.Add(message); + AddEvent(new Events.MessageAddedEvent(Id, message.Id)); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/Message.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/Message.cs new file mode 100644 index 000000000..d9023b3b4 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/Message.cs @@ -0,0 +1,60 @@ +using System; + +namespace MiniSpace.Services.Communication.Core.Entities +{ + public class Message + { + public Guid Id { get; private set; } + public Guid SenderId { get; private set; } + public Guid ReceiverId { get; private set; } + public Guid ChatId { get; private set; } + public string Content { get; private set; } + public DateTime Timestamp { get; private set; } + public MessageType Type { get; private set; } + public MessageStatus Status { get; private set; } + + // Constructor for creating a new message + public Message(Guid chatId, Guid senderId, Guid receiverId, string content, MessageType type) + { + Id = Guid.NewGuid(); // Generate a new ID for a new message + ChatId = chatId; + SenderId = senderId; + ReceiverId = receiverId; + Content = content; + Timestamp = DateTime.UtcNow; + Type = type; + Status = MessageStatus.Sent; + } + + // Constructor for loading an existing message from the database + public Message(Guid id, Guid chatId, Guid senderId, Guid receiverId, string content, DateTime timestamp, MessageType type, MessageStatus status) + { + Id = id; // Use the existing ID from the database + ChatId = chatId; + SenderId = senderId; + ReceiverId = receiverId; + Content = content; + Timestamp = timestamp; + Type = type; + Status = status; + } + + public void MarkAsRead() + { + Status = MessageStatus.Read; + } + + public void MarkAsUnread() + { + if (Status == MessageStatus.Read) + { + Status = MessageStatus.Unread; + } + } + + public void MarkAsDeleted() + { + Status = MessageStatus.Deleted; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/MessageStatus.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/MessageStatus.cs new file mode 100644 index 000000000..54a0ff61f --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/MessageStatus.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Communication.Core.Entities +{ + public enum MessageStatus + { + Sent, + Delivered, + Unread, + Read, + Deleted + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/MessageType.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/MessageType.cs new file mode 100644 index 000000000..67981b777 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/MessageType.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Services.Communication.Core.Entities +{ + public enum MessageType + { + Text, + Image, + Video, + File + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/OrganizationChats.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/OrganizationChats.cs new file mode 100644 index 000000000..b8d75b4f3 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/OrganizationChats.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Communication.Core.Entities +{ + public class OrganizationChats + { + public Guid OrganizationId { get; private set; } + public List Chats { get; private set; } + + public OrganizationChats(Guid organizationId) + { + OrganizationId = organizationId; + Chats = new List(); + } + + public void AddChat(Chat chat) + { + if (chat == null) + throw new ArgumentNullException(nameof(chat)); + + Chats.Add(chat); + } + + public void RemoveChat(Guid chatId) + { + var chat = Chats.FirstOrDefault(c => c.Id == chatId); + if (chat != null) + { + Chats.Remove(chat); + } + } + + public Chat GetChatById(Guid chatId) + { + return Chats.FirstOrDefault(c => c.Id == chatId); + } + + public bool ChatExists(Guid chatId) + { + return Chats.Any(c => c.Id == chatId); + } + + public void UpdateChat(Chat updatedChat) + { + var existingChat = GetChatById(updatedChat.Id); + if (existingChat != null) + { + Chats.Remove(existingChat); + Chats.Add(updatedChat); + } + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/ReactionType.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/ReactionType.cs new file mode 100644 index 000000000..5625742ab --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/ReactionType.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Communication.Core.Entities +{ + public enum ReactionType + { + LoveIt, + LikeIt, + Wow, + ItWasOkay, + HateIt + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/UserChats.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/UserChats.cs new file mode 100644 index 000000000..c77922ee1 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/UserChats.cs @@ -0,0 +1,24 @@ +namespace MiniSpace.Services.Communication.Core.Entities +{ + public class UserChats + { + public Guid UserId { get; private set; } + public List Chats { get; private set; } + + public UserChats(Guid userId) + { + UserId = userId; + Chats = new List(); + } + + public void AddChat(Chat chat) + { + Chats.Add(chat); + } + + public Chat GetChatById(Guid chatId) + { + return Chats.FirstOrDefault(chat => chat.Id == chatId); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/UserMessages.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/UserMessages.cs new file mode 100644 index 000000000..06d5b1cc6 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Entities/UserMessages.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Communication.Core.Entities +{ + public class UserMessages + { + public Guid UserId { get; private set; } + private List _messages; + + public IEnumerable Messages => _messages.AsReadOnly(); + + public UserMessages(Guid userId) + { + UserId = userId; + _messages = new List(); + } + + public void AddMessage(Message message) + { + if (message != null) + { + _messages.Add(message); + } + } + + public void RemoveMessage(Guid messageId) + { + _messages.RemoveAll(m => m.Id == messageId); + } + + public void MarkMessageAsRead(Guid messageId) + { + var message = _messages.FirstOrDefault(m => m.Id == messageId); + if (message != null) + { + message.MarkAsRead(); + } + } + + public void MarkMessageAsUnread(Guid messageId) + { + var message = _messages.FirstOrDefault(m => m.Id == messageId); + if (message != null) + { + message.MarkAsUnread(); + } + } + + public IEnumerable GetMessagesForChat(Guid chatId) + { + return _messages.Where(m => m.ChatId == chatId); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Events/IDomainEvent.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Events/IDomainEvent.cs new file mode 100644 index 000000000..c9c698407 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Events/IDomainEvent.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.Communication.Core.Events +{ + public interface IDomainEvent + { + + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Events/MessageAddedEvent.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Events/MessageAddedEvent.cs new file mode 100644 index 000000000..014121ee9 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Events/MessageAddedEvent.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.Communication.Core.Events +{ + public class MessageAddedEvent : IDomainEvent + { + public Guid ChatId { get; } + public Guid MessageId { get; } + + public MessageAddedEvent(Guid chatId, Guid messageId) + { + ChatId = chatId; + MessageId = messageId; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Exceptions/DomainException.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Exceptions/DomainException.cs new file mode 100644 index 000000000..db88f3978 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Exceptions/DomainException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Communication.Core.Exceptions +{ + public abstract class DomainException : Exception + { + public virtual string Code { get; } + + protected DomainException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Exceptions/InvalidAggregateIdException.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Exceptions/InvalidAggregateIdException.cs new file mode 100644 index 000000000..0c439e479 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Exceptions/InvalidAggregateIdException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Communication.Core.Exceptions +{ + public class InvalidAggregateIdException : DomainException + { + public override string Code { get; } = "invalid_aggregate_id"; + + public InvalidAggregateIdException() : base($"Invalid aggregate id.") + { + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/MiniSpace.Services.Communication.Core.csproj b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/MiniSpace.Services.Communication.Core.csproj new file mode 100644 index 000000000..cf309aa85 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/MiniSpace.Services.Communication.Core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + disable + + + diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/MiniSpace.Services.Communication.Core.sln b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/MiniSpace.Services.Communication.Core.sln new file mode 100644 index 000000000..92b21c3c4 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/MiniSpace.Services.Communication.Core.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Communication.Core", "MiniSpace.Services.Communication.Core.csproj", "{42E0A571-69FB-45ED-BACA-E0F72B9C0DE1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {42E0A571-69FB-45ED-BACA-E0F72B9C0DE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42E0A571-69FB-45ED-BACA-E0F72B9C0DE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42E0A571-69FB-45ED-BACA-E0F72B9C0DE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42E0A571-69FB-45ED-BACA-E0F72B9C0DE1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0A93A2FC-8B65-4C19-A90B-588F58354A02} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Repositories/IOrganizationChatsRepository.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Repositories/IOrganizationChatsRepository.cs new file mode 100644 index 000000000..5fcbf9835 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Repositories/IOrganizationChatsRepository.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Communication.Core.Entities; + +namespace MiniSpace.Services.Communication.Core.Repositories +{ + public interface IOrganizationChatsRepository + { + Task GetByOrganizationIdAsync(Guid organizationId); + Task AddAsync(OrganizationChats organizationChats); + Task UpdateAsync(OrganizationChats organizationChats); + Task AddOrUpdateAsync(OrganizationChats organizationChats); + Task DeleteAsync(Guid organizationId); + Task ChatExistsAsync(Guid organizationId, Guid chatId); + Task AddChatAsync(Guid organizationId, Chat chat); + Task DeleteChatAsync(Guid organizationId, Guid chatId); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Repositories/IUserChatsRepository.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Repositories/IUserChatsRepository.cs new file mode 100644 index 000000000..204c8b668 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Repositories/IUserChatsRepository.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Communication.Core.Entities; + +namespace MiniSpace.Services.Communication.Core.Repositories +{ + public interface IUserChatsRepository + { + Task GetByUserIdAsync(Guid userId); + Task AddAsync(UserChats userChats); + Task UpdateAsync(UserChats userChats); + Task AddOrUpdateAsync(UserChats userChats); + Task DeleteAsync(Guid userId); + Task ChatExistsAsync(Guid userId, Guid chatId); + Task AddChatAsync(Guid userId, Chat chat); + Task DeleteChatAsync(Guid userId, Guid chatId); + Task GetByChatIdAsync(Guid chatId); + Task> GetParticipantIdsByChatIdAsync(Guid chatId); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Wrappers/PagedResponse.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Wrappers/PagedResponse.cs new file mode 100644 index 000000000..586f3a6ab --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Wrappers/PagedResponse.cs @@ -0,0 +1,28 @@ +namespace MiniSpace.Services.Communication.Core.Wrappers +{ + public class PagedResponse + { + public IEnumerable Items { get; } + public int TotalPages { get; } + public int TotalItems { get; } + public int PageSize { get; } + public int Page { get; } + public bool First { get; } + public bool Last { get; } + public bool Empty { get; } + public int? NextPage => Page < TotalPages ? Page + 1 : (int?)null; + public int? PreviousPage => Page > 1 ? Page - 1 : (int?)null; + + public PagedResponse(IEnumerable items, int page, int pageSize, int totalItems) + { + Items = items; + PageSize = pageSize; + TotalItems = totalItems; + TotalPages = pageSize > 0 ? (int)Math.Ceiling((decimal)totalItems / pageSize) : 0; + Page = page; + First = page == 1; + Last = page == TotalPages; + Empty = !items.Any(); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Wrappers/Response.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Wrappers/Response.cs new file mode 100644 index 000000000..92816d60c --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Core/Wrappers/Response.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Services.Communication.Core.Wrappers +{ + public class Response + { + public T Content { get; set; } + public bool Succeeded { get; set; } + public string[] Errors { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/.gitignore b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/.gitignore new file mode 100644 index 000000000..1f7e99963 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/.gitignore @@ -0,0 +1,334 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +# **/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +bin/ +obj/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +logs/ diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/AppContext.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/AppContext.cs new file mode 100644 index 000000000..aa6839fbd --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/AppContext.cs @@ -0,0 +1,27 @@ +using MiniSpace.Services.Communication.Application; + +namespace MiniSpace.Services.Communication.Infrastructure.Contexts +{ + internal class AppContext : IAppContext + { + public string RequestId { get; } + public IIdentityContext Identity { get; } + + internal AppContext() : this(Guid.NewGuid().ToString("N"), IdentityContext.Empty) + { + } + + internal AppContext(CorrelationContext context) : this(context.CorrelationId, + context.User is null ? IdentityContext.Empty : new IdentityContext(context.User)) + { + } + + internal AppContext(string requestId, IIdentityContext identity) + { + RequestId = requestId; + Identity = identity; + } + + internal static IAppContext Empty => new AppContext(); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/AppContextFactory.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/AppContextFactory.cs new file mode 100644 index 000000000..8f300c3d8 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/AppContextFactory.cs @@ -0,0 +1,36 @@ +using Convey.MessageBrokers; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using MiniSpace.Services.Communication.Application; +using MiniSpace.Services.Communication.Infrastructure; + +namespace MiniSpace.Services.Communication.Infrastructure.Contexts +{ + internal sealed class AppContextFactory : IAppContextFactory + { + private readonly ICorrelationContextAccessor _contextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + + public AppContextFactory(ICorrelationContextAccessor contextAccessor, IHttpContextAccessor httpContextAccessor) + { + _contextAccessor = contextAccessor; + _httpContextAccessor = httpContextAccessor; + } + + public IAppContext Create() + { + if (_contextAccessor.CorrelationContext is { }) + { + var payload = JsonConvert.SerializeObject(_contextAccessor.CorrelationContext); + + return string.IsNullOrWhiteSpace(payload) + ? AppContext.Empty + : new AppContext(JsonConvert.DeserializeObject(payload)); + } + + var context = _httpContextAccessor.GetCorrelationContext(); + + return context is null ? AppContext.Empty : new AppContext(context); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/CorrelationContext.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/CorrelationContext.cs new file mode 100644 index 000000000..8c2def9a0 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/CorrelationContext.cs @@ -0,0 +1,22 @@ +namespace MiniSpace.Services.Communication.Infrastructure.Contexts +{ + internal class CorrelationContext + { + public string CorrelationId { get; set; } + public string SpanContext { get; set; } + public UserContext User { get; set; } + public string ResourceId { get; set; } + public string TraceId { get; set; } + public string ConnectionId { get; set; } + public string Name { get; set; } + public DateTime CreatedAt { get; set; } + + public class UserContext + { + public string Id { get; set; } + public bool IsAuthenticated { get; set; } + public string Role { get; set; } + public IDictionary Claims { get; set; } + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/IdentityContext.cs new file mode 100644 index 000000000..1e06fc482 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Contexts/IdentityContext.cs @@ -0,0 +1,39 @@ +using MiniSpace.Services.Communication.Application; + +namespace MiniSpace.Services.Communication.Infrastructure.Contexts +{ + internal class IdentityContext : IIdentityContext + { + public Guid Id { get; } + public string Role { get; } = string.Empty; + public string Name { get; } = string.Empty; + public string Email { get; } = string.Empty; + public bool IsAuthenticated { get; } + public bool IsAdmin { get; } + public bool IsBanned { get; } + public IDictionary Claims { get; } = new Dictionary(); + + internal IdentityContext() + { + } + + internal IdentityContext(CorrelationContext.UserContext context) + : this(context.Id, context.Role, context.IsAuthenticated, context.Claims) + { + } + + internal IdentityContext(string id, string role, bool isAuthenticated, IDictionary claims) + { + Id = Guid.TryParse(id, out var userId) ? userId : Guid.Empty; + Role = role ?? string.Empty; + IsAuthenticated = isAuthenticated; + IsAdmin = Role.Equals("admin", StringComparison.InvariantCultureIgnoreCase); + IsBanned = Role.Equals("banned", StringComparison.InvariantCultureIgnoreCase); + Claims = claims ?? new Dictionary(); + Name = Claims.TryGetValue("name", out var name) ? name : string.Empty; + Email = Claims.TryGetValue("email", out var email) ? email : string.Empty; + } + + internal static IIdentityContext Empty => new IdentityContext(); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs new file mode 100644 index 000000000..849f12031 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Commands; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.Types; + +namespace MiniSpace.Services.Communication.Infrastructure.Decorators +{ + [Decorator] + internal sealed class OutboxCommandHandlerDecorator : ICommandHandler + where TCommand : class, ICommand + { + private readonly ICommandHandler _handler; + private readonly IMessageOutbox _outbox; + private readonly string _messageId; + private readonly bool _enabled; + + public OutboxCommandHandlerDecorator(ICommandHandler handler, IMessageOutbox outbox, + OutboxOptions outboxOptions, IMessagePropertiesAccessor messagePropertiesAccessor) + { + _handler = handler; + _outbox = outbox; + _enabled = outboxOptions.Enabled; + + var messageProperties = messagePropertiesAccessor.MessageProperties; + _messageId = string.IsNullOrWhiteSpace(messageProperties?.MessageId) + ? Guid.NewGuid().ToString("N") + : messageProperties.MessageId; + } + + public Task HandleAsync(TCommand command, CancellationToken cancellationToken) + => _enabled + ? _outbox.HandleAsync(_messageId, () => _handler.HandleAsync(command)) + : _handler.HandleAsync(command); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs new file mode 100644 index 000000000..3e1e88706 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.Types; + +namespace MiniSpace.Services.Communication.Infrastructure.Decorators +{ + [Decorator] + internal sealed class OutboxEventHandlerDecorator : IEventHandler + where TEvent : class, IEvent + { + private readonly IEventHandler _handler; + private readonly IMessageOutbox _outbox; + private readonly string _messageId; + private readonly bool _enabled; + + public OutboxEventHandlerDecorator(IEventHandler handler, IMessageOutbox outbox, + OutboxOptions outboxOptions, IMessagePropertiesAccessor messagePropertiesAccessor) + { + _handler = handler; + _outbox = outbox; + _enabled = outboxOptions.Enabled; + + var messageProperties = messagePropertiesAccessor.MessageProperties; + _messageId = string.IsNullOrWhiteSpace(messageProperties?.MessageId) + ? Guid.NewGuid().ToString("N") + : messageProperties.MessageId; + } + + public Task HandleAsync(TEvent @event, CancellationToken cancellationToken) + => _enabled + ? _outbox.HandleAsync(_messageId, () => _handler.HandleAsync(@event)) + : _handler.HandleAsync(@event); + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Exceptions/ExceptionToMessageMapper.cs new file mode 100644 index 000000000..dd910203c --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -0,0 +1,40 @@ +using Convey.MessageBrokers.RabbitMQ; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Events.Rejected; +using MiniSpace.Services.Communication.Application.Exceptions; +using System; + +namespace MiniSpace.Services.Communication.Infrastructure.Exceptions +{ + internal sealed class ExceptionToMessageMapper : IExceptionToMessageMapper + { + public object Map(Exception exception, object message) + => exception switch + { + // ChatNotFoundException ex => message switch + // { + // DeleteChat command => new ChatDeletionRejected(command.ChatId, "Chat not found", ex.Code), + // CreateChat command => new ChatCreationRejected(command.ChatId, "Chat not found", ex.Code), + // AddUserToChat command => new UserAdditionToChatRejected(command.ChatId, command.UserId, "Chat not found", ex.Code), + // SendMessage command => new MessageSendRejected(command.ChatId, Guid.NewGuid(), "Chat not found", ex.Code), + // _ => new ChatProcessRejected(ex.ChatId, ex.Message, ex.Code), + // }, + // MessageNotFoundException ex => message switch + // { + // DeleteMessage command => new MessageSendRejected(command.ChatId, command.MessageId, "Message not found", ex.Code), + // _ => new MessageProcessRejected(ex.MessageId, ex.Message, ex.Code), + // }, + // InvalidChatOperationException ex => message switch + // { + // AddUserToChat command => new UserAdditionToChatRejected(command.ChatId, command.UserId, ex.Message, ex.Code), + // SendMessage command => new MessageSendRejected(command.ChatId, Guid.NewGuid(), ex.Message, ex.Code), + // _ => new ChatProcessRejected(Guid.Empty, ex.Message, ex.Code), + // }, + // AppException ex => message switch + // { + // _ => new ChatProcessRejected(Guid.Empty, ex.Message, ex.Code) + // }, + // _ => null + }; + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Exceptions/ExceptionToResponseMapper.cs new file mode 100644 index 000000000..6338b7896 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -0,0 +1,46 @@ +using System.Collections.Concurrent; +using System.Net; +using Convey; +using Convey.WebApi.Exceptions; +using MiniSpace.Services.Communication.Application.Exceptions; +using MiniSpace.Services.Communication.Core.Exceptions; + +namespace MiniSpace.Services.Communication.Infrastructure.Exceptions +{ + internal sealed class ExceptionToResponseMapper : IExceptionToResponseMapper + { + private static readonly ConcurrentDictionary Codes = new ConcurrentDictionary(); + + public ExceptionResponse Map(Exception exception) + => exception switch + { + DomainException ex => new ExceptionResponse(new {code = GetCode(ex), reason = ex.Message}, + HttpStatusCode.BadRequest), + AppException ex => new ExceptionResponse(new {code = GetCode(ex), reason = ex.Message}, + HttpStatusCode.BadRequest), + _ => new ExceptionResponse(new {code = "error", reason = "There was an error."}, + HttpStatusCode.BadRequest) + }; + + private static string GetCode(Exception exception) + { + var type = exception.GetType(); + if (Codes.TryGetValue(type, out var code)) + { + return code; + } + + var exceptionCode = exception switch + { + DomainException domainException when !string.IsNullOrWhiteSpace(domainException.Code) => domainException + .Code, + AppException appException when !string.IsNullOrWhiteSpace(appException.Code) => appException.Code, + _ => exception.GetType().Name.Underscore().Replace("_exception", string.Empty) + }; + + Codes.TryAdd(type, exceptionCode); + + return exceptionCode; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Extensions.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Extensions.cs new file mode 100644 index 000000000..a0779ab56 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Extensions.cs @@ -0,0 +1,165 @@ +using System.Text; +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using Convey.CQRS.Queries; +using Convey.Discovery.Consul; +using Convey.Docs.Swagger; +using Convey.HTTP; +using Convey.LoadBalancing.Fabio; +using Convey.MessageBrokers; +using Convey.MessageBrokers.CQRS; +using Convey.MessageBrokers.Outbox; +using Convey.MessageBrokers.Outbox.Mongo; +using Convey.MessageBrokers.RabbitMQ; +using Convey.Metrics.AppMetrics; +using Convey.Persistence.MongoDB; +using Convey.Persistence.Redis; +using Convey.Security; +using Convey.Tracing.Jaeger; +using Convey.Tracing.Jaeger.RabbitMQ; +using Convey.WebApi; +using Convey.WebApi.CQRS; +using Convey.WebApi.Security; +using Convey.WebApi.Swagger; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using MiniSpace.Services.Communication.Application; +using MiniSpace.Services.Communication.Application.Commands; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Core.Repositories; +using MiniSpace.Services.Communication.Infrastructure.Contexts; +using MiniSpace.Services.Communication.Infrastructure.Decorators; +using MiniSpace.Services.Communication.Infrastructure.Exceptions; +using MiniSpace.Services.Communication.Infrastructure.Logging; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Repositories; +using MiniSpace.Services.Communication.Infrastructure.Services; +using MiniSpace.Services.Communication.Application.Services.Clients; +using MiniSpace.Services.Communication.Infrastructure.Services.Clients; +using MiniSpace.Services.Communication.Application.Hubs; + +namespace MiniSpace.Services.Communication.Infrastructure +{ + public static class Extensions + { + public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(ctx => ctx.GetRequiredService().Create()); + builder.Services.TryDecorate(typeof(ICommandHandler<>), typeof(OutboxCommandHandlerDecorator<>)); + builder.Services.TryDecorate(typeof(IEventHandler<>), typeof(OutboxEventHandlerDecorator<>)); + + + return builder + .AddErrorHandler() + .AddQueryHandlers() + .AddInMemoryQueryDispatcher() + .AddHttpClient() + .AddConsul() + .AddFabio() + .AddRabbitMq(plugins: p => p.AddJaegerRabbitMqPlugin()) + .AddMessageOutbox(o => o.AddMongo()) + .AddExceptionToMessageMapper() + .AddMongo() + .AddRedis() + .AddMetrics() + .AddJaeger() + .AddHandlersLogging() + .AddMongoRepository("organizations_chats") + .AddMongoRepository("user_chats") + .AddSignalRInfrastructure() + .AddWebApiSwaggerDocs() + .AddCertificateAuthentication() + .AddSecurity(); + } + + public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) + { + app.UseErrorHandler() + .UseSwaggerDocs() + .UseJaeger() + .UseConvey() + .UsePublicContracts() + .UseMetrics() + .UseCertificateAuthentication() + .UseRabbitMq() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + ; + return app; + } + + public static IConveyBuilder AddSignalRInfrastructure(this IConveyBuilder builder) + { + builder.Services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials() + .SetIsOriginAllowed((host) => true)); + }); + + builder.Services.AddSignalR(); + + return builder; + } + + + internal static CorrelationContext GetCorrelationContext(this IHttpContextAccessor accessor) + => accessor.HttpContext?.Request.Headers.TryGetValue("Correlation-Context", out var json) is true + ? JsonConvert.DeserializeObject(json.FirstOrDefault()) + : null; + + internal static IDictionary GetHeadersToForward(this IMessageProperties messageProperties) + { + const string sagaHeader = "Saga"; + if (messageProperties?.Headers is null || !messageProperties.Headers.TryGetValue(sagaHeader, out var saga)) + { + return null; + } + + return saga is null + ? null + : new Dictionary + { + [sagaHeader] = saga + }; + } + + internal static string GetSpanContext(this IMessageProperties messageProperties, string header) + { + if (messageProperties is null) + { + return string.Empty; + } + + if (messageProperties.Headers.TryGetValue(header, out var span) && span is byte[] spanBytes) + { + return Encoding.UTF8.GetString(spanBytes); + } + + return string.Empty; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/IAppContextFactory.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/IAppContextFactory.cs new file mode 100644 index 000000000..64a4521dc --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/IAppContextFactory.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Communication.Application; + +namespace MiniSpace.Services.Communication.Infrastructure +{ + public interface IAppContextFactory + { + IAppContext Create(); + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Logging/Extensions.cs similarity index 63% rename from MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/Extensions.cs rename to MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Logging/Extensions.cs index 37c5ee989..ef9726c4f 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/Extensions.cs +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Logging/Extensions.cs @@ -1,18 +1,19 @@ using Convey; using Convey.Logging.CQRS; using Microsoft.Extensions.DependencyInjection; -using MiniSpace.Services.Friends.Application.Commands; +using MiniSpace.Services.Communication.Application.Commands; +using System.Reflection; -namespace MiniSpace.Services.Friends.Infrastructure.Logging +namespace MiniSpace.Services.Communication.Infrastructure.Logging { internal static class Extensions { public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) { - var assembly = typeof(AddFriend).Assembly; - - builder.Services.AddSingleton(new MessageToLogTemplateMapper()); - + var assembly = typeof(UpdateMessageStatus).Assembly; + + builder.Services.AddSingleton(); + return builder .AddCommandHandlersLogging(assembly) .AddEventHandlersLogging(assembly); diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Logging/MessageToLogTemplateMapper.cs new file mode 100644 index 000000000..bfe40da1b --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -0,0 +1,77 @@ +using Convey.Logging.CQRS; +using Microsoft.Extensions.Logging; +using MiniSpace.Services.Communication.Application.Commands; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Infrastructure.Logging +{ + internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper + { + private readonly ILogger _logger; + + private static IReadOnlyDictionary MessageTemplates => new Dictionary + { + { + typeof(AddUserToChat), new HandlerLogTemplate + { + After = "Added user with id: {UserId} to chat with id: {ChatId}." + } + }, + { + typeof(CreateChat), new HandlerLogTemplate + { + After = "Created chat with id: {ChatId}, participants: {ParticipantIds}, and name: '{ChatName}'." + } + }, + { + typeof(DeleteChat), new HandlerLogTemplate + { + After = "Deleted chat with id: {ChatId}." + } + }, + { + typeof(DeleteMessage), new HandlerLogTemplate + { + After = "Deleted message with id: {MessageId} from chat with id: {ChatId}." + } + }, + { + typeof(RemoveUserFromChat), new HandlerLogTemplate + { + After = "Removed user with id: {UserId} from chat with id: {ChatId}." + } + }, + { + typeof(SendMessage), new HandlerLogTemplate + { + After = "Sent message in chat with id: {ChatId} by user with id: {SenderId}, content: '{Content}', and type: '{MessageType}'." + } + }, + { + typeof(UpdateMessageStatus), new HandlerLogTemplate + { + After = "Updated message status to '{Status}' for message with id: {MessageId} in chat with id: {ChatId}." + } + } + }; + + public MessageToLogTemplateMapper(ILogger logger) + { + _logger = logger; + } + + public HandlerLogTemplate Map(TMessage message) where TMessage : class + { + var messageType = message.GetType(); + _logger.LogInformation($"Attempting to map message type: {messageType.Name}"); + if (MessageTemplates.TryGetValue(messageType, out var template)) + { + _logger.LogInformation($"Mapping found. Template: {template.After}"); + return template; + } + _logger.LogWarning($"No mapping found for message type: {messageType.Name}"); + return null; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/MiniSpace.Services.Communication.Infrastructure.csproj b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/MiniSpace.Services.Communication.Infrastructure.csproj new file mode 100644 index 000000000..4992a6cf3 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/MiniSpace.Services.Communication.Infrastructure.csproj @@ -0,0 +1,40 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/MiniSpace.Services.Communication.Infrastructure.sln b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/MiniSpace.Services.Communication.Infrastructure.sln new file mode 100644 index 000000000..4024f4b05 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/MiniSpace.Services.Communication.Infrastructure.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniSpace.Services.Communication.Infrastructure", "MiniSpace.Services.Communication.Infrastructure.csproj", "{994B662D-5C21-4077-8970-C61A227B46D4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {994B662D-5C21-4077-8970-C61A227B46D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {994B662D-5C21-4077-8970-C61A227B46D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {994B662D-5C21-4077-8970-C61A227B46D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {994B662D-5C21-4077-8970-C61A227B46D4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {ADCE09CD-5371-42F5-9AB2-04FCDA89D8EA} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/ChatDocument.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/ChatDocument.cs new file mode 100644 index 000000000..98bf65ee4 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/ChatDocument.cs @@ -0,0 +1,25 @@ +using Convey.Types; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Documents +{ + public class ChatDocument : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + + public List ParticipantIds { get; set; } + + public List Messages { get; set; } + + public ChatDocument() + { + ParticipantIds = new List(); + Messages = new List(); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/Extensions.cs new file mode 100644 index 000000000..c82c4f595 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/Extensions.cs @@ -0,0 +1,84 @@ +using MiniSpace.Services.Communication.Application.Dto; +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Documents +{ + public static class Extensions +{ + // Converts MessageDocument to Message entity + public static Message AsEntity(this MessageDocument document) + { + // Use the existing ID and other properties directly from the document + return new Message( + document.Id, // Use the existing ID from the document + document.ChatId, + document.SenderId, + document.ReceiverId, + document.Content, + document.Timestamp, + document.Type, + document.Status // Ensure the status is loaded correctly + ); + } + + public static MessageDocument AsDocument(this Message entity) + { + return new MessageDocument + { + Id = entity.Id, + ChatId = entity.ChatId, + SenderId = entity.SenderId, + ReceiverId = entity.ReceiverId, + Content = entity.Content, + Timestamp = entity.Timestamp, + Type = entity.Type, + Status = entity.Status + }; + } + + public static Chat AsEntity(this ChatDocument document) + { + var messages = document.Messages.Select(m => m.AsEntity()).ToList(); + return new Chat(document.Id, document.ParticipantIds, messages); + } + + public static ChatDocument AsDocument(this Chat entity) + { + return new ChatDocument + { + Id = entity.Id, + ParticipantIds = entity.ParticipantIds, + Messages = entity.Messages.Select(m => m.AsDocument()).ToList() + }; + } + + public static ChatDto AsDto(this Chat entity) + { + return new ChatDto + { + Id = entity.Id, + ParticipantIds = entity.ParticipantIds, + Messages = entity.Messages.Select(m => m.AsDto()).ToList() + }; + } + + public static MessageDto AsDto(this Message entity) + { + return new MessageDto + { + Id = entity.Id, // Ensure the ID is correctly mapped + ChatId = entity.ChatId, + SenderId = entity.SenderId, + Content = entity.Content, + Timestamp = entity.Timestamp, + MessageType = entity.Type.ToString(), + Status = entity.Status.ToString() + }; + } +} + + } diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/MessageDocument.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/MessageDocument.cs new file mode 100644 index 000000000..24ecd3022 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/MessageDocument.cs @@ -0,0 +1,22 @@ +using Convey.Types; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MiniSpace.Services.Communication.Core.Entities; +using System; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Documents +{ + public class MessageDocument : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + public Guid ChatId { get; set; } + public Guid SenderId { get; set; } + public Guid ReceiverId { get; set; } + public string Content { get; set; } + public DateTime Timestamp { get; set; } + public MessageType Type { get; set; } + public MessageStatus Status { get; set; } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/OrganizationChatsDocument.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/OrganizationChatsDocument.cs new file mode 100644 index 000000000..f65e70d73 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/OrganizationChatsDocument.cs @@ -0,0 +1,21 @@ +using Convey.Types; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Documents +{ + public class OrganizationChatsDocument : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + public List Chats { get; set; } + public OrganizationChatsDocument() + { + Chats = new List(); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/OrganizationChatsExtensions.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/OrganizationChatsExtensions.cs new file mode 100644 index 000000000..7be5c2722 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/OrganizationChatsExtensions.cs @@ -0,0 +1,32 @@ +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Documents +{ + public static class OrganizationChatsExtensions + { + public static OrganizationChats AsEntity(this OrganizationChatsDocument document) + { + var organizationChats = new OrganizationChats(document.OrganizationId); + foreach (var chatDocument in document.Chats) + { + var chat = chatDocument.AsEntity(); + organizationChats.AddChat(chat); + } + return organizationChats; + } + + public static OrganizationChatsDocument AsDocument(this OrganizationChats entity) + { + return new OrganizationChatsDocument + { + Id = Guid.NewGuid(), + OrganizationId = entity.OrganizationId, + Chats = entity.Chats.Select(c => c.AsDocument()).ToList() + }; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/UserChatsDocument.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/UserChatsDocument.cs new file mode 100644 index 000000000..8e39b3f25 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/UserChatsDocument.cs @@ -0,0 +1,21 @@ +using Convey.Types; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Documents +{ + public class UserChatsDocument : IIdentifiable + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + public Guid UserId { get; set; } + public List Chats { get; set; } + public UserChatsDocument() + { + Chats = new List(); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/UserChatsExtensions.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/UserChatsExtensions.cs new file mode 100644 index 000000000..a1a702e2b --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Documents/UserChatsExtensions.cs @@ -0,0 +1,32 @@ +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Documents +{ + public static class UserChatsExtensions + { + public static UserChats AsEntity(this UserChatsDocument document) + { + var userChats = new UserChats(document.UserId); + foreach (var chatDocument in document.Chats) + { + var chat = chatDocument.AsEntity(); + userChats.AddChat(chat); + } + return userChats; + } + + public static UserChatsDocument AsDocument(this UserChats entity) + { + return new UserChatsDocument + { + Id = Guid.NewGuid(), + UserId = entity.UserId, + Chats = entity.Chats.Select(c => c.AsDocument()).ToList() + }; + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetChatByIdHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetChatByIdHandler.cs new file mode 100644 index 000000000..bd7bb4376 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetChatByIdHandler.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Communication.Application.Dto; +using MiniSpace.Services.Communication.Application.Queries; +using MiniSpace.Services.Communication.Core.Repositories; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Queries.Handlers +{ + public class GetChatByIdHandler : IQueryHandler + { + private readonly IUserChatsRepository _userChatsRepository; + private readonly IOrganizationChatsRepository _organizationChatsRepository; + + public GetChatByIdHandler(IUserChatsRepository userChatsRepository, IOrganizationChatsRepository organizationChatsRepository) + { + _userChatsRepository = userChatsRepository; + _organizationChatsRepository = organizationChatsRepository; + } + + public async Task HandleAsync(GetChatById query, CancellationToken cancellationToken) + { + var userChat = await _userChatsRepository.GetByUserIdAsync(query.ChatId); + var chat = userChat?.GetChatById(query.ChatId) ?? (await _organizationChatsRepository.GetByOrganizationIdAsync(query.ChatId))?.GetChatById(query.ChatId); + + return chat?.AsDocument().AsEntity().AsDto(); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetMessagesForChatHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetMessagesForChatHandler.cs new file mode 100644 index 000000000..84c91211f --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetMessagesForChatHandler.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Communication.Application.Dto; +using MiniSpace.Services.Communication.Application.Queries; +using MiniSpace.Services.Communication.Core.Repositories; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; + + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Queries.Handlers +{ + public class GetMessagesForChatHandler : IQueryHandler> + { + private readonly IUserChatsRepository _userChatsRepository; + + public GetMessagesForChatHandler(IUserChatsRepository userChatsRepository) + { + _userChatsRepository = userChatsRepository; + } + + public async Task> HandleAsync(GetMessagesForChat query, CancellationToken cancellationToken) + { + var chat = await _userChatsRepository.GetByChatIdAsync(query.ChatId); + + if (chat != null) + { + var messages = chat.Messages.Select(m => m.AsDto()).ToList(); + return messages; + } + + return Enumerable.Empty(); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetUserChatsHandler.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetUserChatsHandler.cs new file mode 100644 index 000000000..aa0ede7b9 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Queries/Handlers/GetUserChatsHandler.cs @@ -0,0 +1,45 @@ +using System.Linq; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Communication.Application.Dto; +using MiniSpace.Services.Communication.Application.Queries; +using MiniSpace.Services.Communication.Core.Repositories; +using MiniSpace.Services.Communication.Core.Wrappers; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Queries.Handlers +{ + public class GetUserChatsHandler : IQueryHandler> + { + private readonly IUserChatsRepository _userChatsRepository; + + public GetUserChatsHandler(IUserChatsRepository userChatsRepository) + { + _userChatsRepository = userChatsRepository; + } + + public async Task> HandleAsync(GetUserChats query, CancellationToken cancellationToken) + { + var userChats = await _userChatsRepository.GetByUserIdAsync(query.UserId); + + if (userChats == null || !userChats.Chats.Any()) + { + return new PagedResponse(Enumerable.Empty(), 0, query.PageSize, 0); + } + + var paginatedChats = userChats.Chats + .Skip((query.Page - 1) * query.PageSize) + .Take(query.PageSize) + .Select(chat => chat.AsDto()) + .ToList(); + + var userChatDto = new UserChatDto + { + UserId = query.UserId, + Chats = paginatedChats + }; + + return new PagedResponse(new List { userChatDto }, query.Page, query.PageSize, userChats.Chats.Count); + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Repositories/OrganizationChatsRepository.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Repositories/OrganizationChatsRepository.cs new file mode 100644 index 000000000..909cd3a47 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Repositories/OrganizationChatsRepository.cs @@ -0,0 +1,81 @@ +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Core.Repositories; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using Convey.Persistence.MongoDB; +using System; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Repositories +{ + public class OrganizationChatsRepository : IOrganizationChatsRepository + { + private readonly IMongoRepository _repository; + + public OrganizationChatsRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetByOrganizationIdAsync(Guid organizationId) + { + var document = await _repository.GetAsync(x => x.OrganizationId == organizationId); + return document?.AsEntity(); + } + + public async Task AddAsync(OrganizationChats organizationChats) + { + await _repository.AddAsync(organizationChats.AsDocument()); + } + + public async Task UpdateAsync(OrganizationChats organizationChats) + { + await _repository.UpdateAsync(organizationChats.AsDocument()); + } + + public async Task AddOrUpdateAsync(OrganizationChats organizationChats) + { + var existingDocument = await _repository.GetAsync(x => x.OrganizationId == organizationChats.OrganizationId); + if (existingDocument == null) + { + await AddAsync(organizationChats); + } + else + { + await UpdateAsync(organizationChats); + } + } + + public async Task DeleteAsync(Guid organizationId) + { + await _repository.DeleteAsync(x => x.OrganizationId == organizationId); + } + + public async Task ChatExistsAsync(Guid organizationId, Guid chatId) + { + var document = await _repository.GetAsync(x => x.OrganizationId == organizationId && x.Chats.Any(c => c.Id == chatId)); + return document != null; + } + + public async Task AddChatAsync(Guid organizationId, Chat chat) + { + var organizationChats = await GetByOrganizationIdAsync(organizationId) ?? new OrganizationChats(organizationId); + organizationChats.AddChat(chat); + await AddOrUpdateAsync(organizationChats); + } + + public async Task DeleteChatAsync(Guid organizationId, Guid chatId) + { + var organizationChats = await GetByOrganizationIdAsync(organizationId); + if (organizationChats != null) + { + var chat = organizationChats.GetChatById(chatId); + if (chat != null) + { + organizationChats.Chats.Remove(chat); + await UpdateAsync(organizationChats); + } + } + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Repositories/UserChatsRepository.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Repositories/UserChatsRepository.cs new file mode 100644 index 000000000..1db100707 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Mongo/Repositories/UserChatsRepository.cs @@ -0,0 +1,106 @@ +using MiniSpace.Services.Communication.Core.Entities; +using MiniSpace.Services.Communication.Core.Repositories; +using MiniSpace.Services.Communication.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using Convey.Persistence.MongoDB; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Communication.Infrastructure.Mongo.Repositories +{ + public class UserChatsRepository : IUserChatsRepository + { + private readonly IMongoRepository _repository; + + public UserChatsRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetByUserIdAsync(Guid userId) + { + var document = await _repository.GetAsync(x => x.UserId == userId); + return document?.AsEntity(); + } + + public async Task GetByChatIdAsync(Guid chatId) + { + var document = await _repository.Collection.Find(x => x.Chats.Any(c => c.Id == chatId)).FirstOrDefaultAsync(); + var chatDocument = document?.Chats.FirstOrDefault(c => c.Id == chatId); + return chatDocument?.AsEntity(); + } + + public async Task AddAsync(UserChats userChats) + { + await _repository.AddAsync(userChats.AsDocument()); + } + + public async Task UpdateAsync(UserChats userChats) + { + var filter = Builders.Filter.Eq(doc => doc.UserId, userChats.UserId); + var update = Builders.Update + .Set(doc => doc.Chats, userChats.Chats.Select(chat => chat.AsDocument()).ToList()); + + await _repository.Collection.UpdateOneAsync(filter, update); + } + + public async Task AddOrUpdateAsync(UserChats userChats) + { + var existingDocument = await _repository.GetAsync(x => x.UserId == userChats.UserId); + if (existingDocument == null) + { + await AddAsync(userChats); + } + else + { + await UpdateAsync(userChats); + } + } + + public async Task DeleteAsync(Guid userId) + { + await _repository.DeleteAsync(x => x.UserId == userId); + } + + public async Task ChatExistsAsync(Guid userId, Guid chatId) + { + var document = await _repository.GetAsync(x => x.UserId == userId && x.Chats.Any(c => c.Id == chatId)); + return document != null; + } + + public async Task AddChatAsync(Guid userId, Chat chat) + { + var userChats = await GetByUserIdAsync(userId) ?? new UserChats(userId); + userChats.AddChat(chat); + await AddOrUpdateAsync(userChats); + } + + public async Task DeleteChatAsync(Guid userId, Guid chatId) + { + var userChats = await GetByUserIdAsync(userId); + if (userChats == null) + { + return; + } + var chat = userChats.GetChatById(chatId); + if (chat == null) + { + return; + } + userChats.Chats.Remove(chat); + await UpdateAsync(userChats); + } + + public async Task> GetParticipantIdsByChatIdAsync(Guid chatId) + { + var document = await _repository.Collection + .Find(x => x.Chats.Any(c => c.Id == chatId)) + .FirstOrDefaultAsync(); + + var chatDocument = document?.Chats.FirstOrDefault(c => c.Id == chatId); + return chatDocument?.ParticipantIds ?? new List(); + } + + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/Clients/StudentsServiceClient.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/Clients/StudentsServiceClient.cs new file mode 100644 index 000000000..2c20e4f2b --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/Clients/StudentsServiceClient.cs @@ -0,0 +1,25 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Convey.HTTP; +using MiniSpace.Services.Communication.Application.Dto; +using MiniSpace.Services.Communication.Application.Queries; +using MiniSpace.Services.Communication.Application.Services.Clients; + +namespace MiniSpace.Services.Communication.Infrastructure.Services.Clients +{ + public class StudentsServiceClient : IStudentsServiceClient + { + private readonly IHttpClient _httpClient; + private readonly string _url; + + public StudentsServiceClient(IHttpClient httpClient, HttpClientOptions options) + { + _httpClient = httpClient; + _url = options.Services["students"]; + } + + public Task GetAsync(Guid id) + => _httpClient.GetAsync($"{_url}/students/{id}"); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/DateTimeProvider.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/DateTimeProvider.cs new file mode 100644 index 000000000..692cc86f0 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/DateTimeProvider.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.Communication.Application.Services; + +namespace MiniSpace.Services.Communication.Infrastructure.Services +{ + internal sealed class DateTimeProvider : IDateTimeProvider + { + public DateTime Now => DateTime.UtcNow; + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/EventMapper.cs new file mode 100644 index 000000000..cfbb8a5b6 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/EventMapper.cs @@ -0,0 +1,32 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Communication.Application.Events; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Core.Events; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Communication.Infrastructure.Services +{ + public class EventMapper : IEventMapper + { + public IEnumerable MapAll(IEnumerable events) + => events.Select(Map).Where(mappedEvent => mappedEvent != null); + + public IEvent Map(IDomainEvent @event) + { + switch (@event) + { + case MessageAddedEvent e: + return new MessageSent(e.ChatId, e.MessageId, Guid.Empty, string.Empty); + + // Add more cases for other domain events + // case SomeOtherDomainEvent e: + // return new SomeOtherIntegrationEvent(...); + + default: + return null; + } + } + } +} diff --git a/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/MessageBroker.cs new file mode 100644 index 000000000..4bb6cbdd4 --- /dev/null +++ b/MiniSpace.Services.Communication/src/MiniSpace.Services.Communication.Infrastructure/Services/MessageBroker.cs @@ -0,0 +1,94 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.MessageBrokers.RabbitMQ; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using OpenTracing; +using MiniSpace.Services.Communication.Application.Services; +using MiniSpace.Services.Communication.Infrastructure; +using System.Text.Json; + +namespace MiniSpace.Services.Communication.Infrastructure.Services +{ + internal sealed class MessageBroker : IMessageBroker + { + private const string DefaultSpanContextHeader = "span_context"; + private readonly IBusPublisher _busPublisher; + private readonly IMessageOutbox _outbox; + private readonly ICorrelationContextAccessor _contextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IMessagePropertiesAccessor _messagePropertiesAccessor; + private readonly ITracer _tracer; + private readonly ILogger _logger; + private readonly string _spanContextHeader; + + public MessageBroker(IBusPublisher busPublisher, IMessageOutbox outbox, + ICorrelationContextAccessor contextAccessor, IHttpContextAccessor httpContextAccessor, + IMessagePropertiesAccessor messagePropertiesAccessor, RabbitMqOptions options, ITracer tracer, + ILogger logger) + { + _busPublisher = busPublisher; + _outbox = outbox; + _contextAccessor = contextAccessor; + _httpContextAccessor = httpContextAccessor; + _messagePropertiesAccessor = messagePropertiesAccessor; + _tracer = tracer; + _logger = logger; + _spanContextHeader = string.IsNullOrWhiteSpace(options.SpanContextHeader) + ? DefaultSpanContextHeader + : options.SpanContextHeader; + } + + public Task PublishAsync(params IEvent[] events) => PublishAsync(events?.AsEnumerable()); + + public async Task PublishAsync(IEnumerable events) + { + if (events is null) + { + return; + } + + var messageProperties = _messagePropertiesAccessor.MessageProperties; + var originatedMessageId = messageProperties?.MessageId; + var correlationId = messageProperties?.CorrelationId; + var spanContext = messageProperties?.GetSpanContext(_spanContextHeader); + if (string.IsNullOrWhiteSpace(spanContext)) + { + spanContext = _tracer.ActiveSpan is null ? string.Empty : _tracer.ActiveSpan.Context.ToString(); + } + + var headers = messageProperties.GetHeadersToForward(); + var correlationContext = _contextAccessor.CorrelationContext ?? + _httpContextAccessor.GetCorrelationContext(); + + foreach (var @event in events) + { + if (@event is null) + { + continue; + } + + var messageId = Guid.NewGuid().ToString("N"); + _logger.LogTrace($"Publishing integration event: {@event.GetType().Name} [id: '{messageId}']."); + var serializedEvent = JsonSerializer.Serialize(@event, new JsonSerializerOptions + { + WriteIndented = true, // To make it easier to read in logs + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + _logger.LogTrace($"Publishing integration event: {@event.GetType().Name} [id: '{messageId}'], Content: {serializedEvent}"); + // Console.WriteLine($"Publishing Event: {@event.GetType().Name} with ID: {messageId}, Content: {serializedEvent}"); + if (_outbox.Enabled) + { + await _outbox.SendAsync(@event, originatedMessageId, messageId, correlationId, spanContext, + correlationContext, headers); + continue; + } + + await _busPublisher.PublishAsync(@event, messageId, correlationId, spanContext, correlationContext, + headers); + } + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs index 802da81d1..42c9125f5 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs @@ -17,6 +17,7 @@ using MiniSpace.Services.Events.Application.Services; using MiniSpace.Services.Events.Application.Wrappers; using MiniSpace.Services.Events.Infrastructure; +using MiniSpace.Services.Events.Core.Wrappers; using System; using System.IO; using Microsoft.AspNetCore.Builder; @@ -36,16 +37,9 @@ public static async Task Main(string[] args) .AddInfrastructure() .Build()) .Configure(app => app - // .UseMiddleware() - // .UseMiddleware() .UseInfrastructure() .UseEndpoints(endpoints => endpoints - .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) - // .Post("events/search", async (cmd, ctx) => - // { - // var pagedResult = await ctx.RequestServices.GetService().BrowseEventsAsync(cmd); - // await ctx.Response.WriteJsonAsync(pagedResult); - // }) + .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) .Post("events/search/organizer", async (cmd, ctx) => { var pagedResult = await ctx.RequestServices.GetService().BrowseOrganizerEventsAsync(cmd); @@ -53,13 +47,14 @@ public static async Task Main(string[] args) })) .UseDispatcherEndpoints(endpoints => endpoints .Get("events/{eventId}") - .Get>("events/users/{userId}") + .Get>("events/users/{userId}") .Get("events/{eventId}/participants") .Get("events/{eventId}/rating") - .Get>("events/paginated") - .Get>("events/organizer/{organizerId}/paginated") - .Get>("events/search") - + .Get>("events/paginated") + .Get>("events/organizer/{organizerId}/paginated") + .Get>("events/search") + .Get>("events/users/{userId}/feed") + .Get>("events/users/{userId}/views/paginated") .Put("events/{eventId}") .Post("events", afterDispatch: (cmd, ctx) => ctx.Response.Created($"events/{cmd.EventId}")) @@ -71,6 +66,7 @@ public static async Task Main(string[] args) .Post("events/{eventId}/rate") .Delete("events/{eventId}/rate") .Post("events/{eventId}/participants") + .Post("events/{eventId}/view") .Delete("events/{eventId}/participants") ) ) @@ -78,75 +74,4 @@ public static async Task Main(string[] args) .Build() .RunAsync(); } - - public class RequestLoggingMiddleware - { - private readonly RequestDelegate _next; - - public RequestLoggingMiddleware(RequestDelegate next) - { - _next = next; - } - - public async Task InvokeAsync(HttpContext context) - { - // Enable buffering so the stream can be read multiple times - context.Request.EnableBuffering(); - - // Read the stream as text - var bodyAsText = await new StreamReader(context.Request.Body).ReadToEndAsync(); - - // Log the request body - Console.WriteLine("Received JSON:"); - Console.WriteLine(bodyAsText); - - // Reset the stream position to allow the next middleware to read it - context.Request.Body.Position = 0; - - // Call the next middleware in the pipeline - await _next(context); - } - } - - public class ExceptionHandlingMiddleware -{ - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) - { - _next = next; - _logger = logger; - } - - public async Task InvokeAsync(HttpContext context) - { - try - { - await _next(context); - } - catch (Exception ex) - { - _logger.LogError(ex, "An unhandled exception occurred."); - await HandleExceptionAsync(context, ex); - } - } - - private static Task HandleExceptionAsync(HttpContext context, Exception exception) - { - var statusCode = StatusCodes.Status500InternalServerError; - var result = JsonSerializer.Serialize(new { error = exception.Message }); - - if (exception is ArgumentException || exception is InvalidOperationException) - { - statusCode = StatusCodes.Status400BadRequest; - } - - context.Response.ContentType = "application/json"; - context.Response.StatusCode = statusCode; - return context.Response.WriteAsync(result); - } -} - - } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/ViewEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/ViewEventHandler.cs new file mode 100644 index 000000000..a99f9cff2 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/ViewEventHandler.cs @@ -0,0 +1,63 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; +using Microsoft.Extensions.Logging; + +namespace MiniSpace.Services.Events.Application.Commands.Handlers +{ + public class ViewEventHandler : ICommandHandler + { + private readonly IEventsUserViewsRepository _eventsUserViewsRepository; + private readonly IEventRepository _eventRepository; + private readonly ILogger _logger; + + public ViewEventHandler( + IEventsUserViewsRepository eventsUserViewsRepository, + IEventRepository eventRepository, + ILogger logger) + { + _eventsUserViewsRepository = eventsUserViewsRepository; + _eventRepository = eventRepository; + _logger = logger; + } + + public async Task HandleAsync(ViewEvent command, CancellationToken cancellationToken) + { + // Ensure the event exists + var eventExists = await _eventRepository.ExistsAsync(command.EventId); + if (!eventExists) + { + _logger.LogWarning($"Event with ID {command.EventId} not found."); + return; + } + + // Fetch the user's event views + var userViews = await _eventsUserViewsRepository.GetAsync(command.UserId); + if (userViews == null) + { + // If no views exist, create a new EventsViews object for the user + userViews = new EventsViews(command.UserId, Enumerable.Empty()); + } + + // Check if the event has already been viewed + var existingView = userViews.Views.FirstOrDefault(v => v.EventId == command.EventId); + if (existingView != null) + { + // Remove the existing view (to update the date) + userViews.RemoveView(command.EventId); + } + + // Add the new view with the current date + userViews.AddView(command.EventId, DateTime.UtcNow); + + // Save the updated views to the repository + await _eventsUserViewsRepository.UpdateAsync(userViews); + + _logger.LogInformation($"User {command.UserId} viewed event {command.EventId}."); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/ViewEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/ViewEvent.cs new file mode 100644 index 000000000..d1a668dea --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/ViewEvent.cs @@ -0,0 +1,17 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Events.Application.Commands +{ + public class ViewEvent : ICommand + { + public Guid UserId { get; } + public Guid EventId { get; } + + public ViewEvent(Guid userId, Guid eventId) + { + UserId = userId; + EventId = eventId; + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EducationDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EducationDto.cs new file mode 100644 index 000000000..26ce34bd1 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EducationDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Events.Application.DTO +{ + [ExcludeFromCodeCoverage] + public class EducationDto + { + public string InstitutionName { get; set; } + public string Degree { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/FriendDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/FriendDto.cs index 23aeb3968..4daa0d38e 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/FriendDto.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/FriendDto.cs @@ -7,9 +7,9 @@ namespace MiniSpace.Services.Events.Application.DTO public class FriendDto { public Guid Id { get; set; } - public Guid StudentId { get; set; } + public Guid UserId { get; set; } public Guid FriendId { get; set; } public DateTime CreatedAt { get; set; } - public string FriendState { get; set; } + public string State { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/PagedResult.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/PagedResult.cs deleted file mode 100644 index cad6a334d..000000000 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/PagedResult.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MiniSpace.Services.Events.Application.DTO -{ - public class PagedResult - { - public IEnumerable Items { get; } - public int Page { get; } - public int PageSize { get; } - public int TotalItems { get; } - public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((decimal)TotalItems / PageSize) : 0; - - public int? NextPage => Page < TotalPages ? Page + 1 : (int?)null; - public int? PreviousPage => Page > 1 ? Page - 1 : (int?)null; - - public PagedResult(IEnumerable items, int page, int pageSize, int totalItems) - { - Items = items; - Page = page; - PageSize = pageSize; - TotalItems = totalItems; - } - } -} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentDto.cs index ca61ea03c..2e8f31f6f 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentDto.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentDto.cs @@ -7,6 +7,5 @@ namespace MiniSpace.Services.Events.Application.DTO public class StudentDto { public Guid Id { get; set; } - public string Name { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserEventsViewsDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserEventsViewsDto.cs new file mode 100644 index 000000000..47eb5d429 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserEventsViewsDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Events.Application.DTO +{ + public class UserEventsViewsDto + { + public Guid UserId { get; set; } + public IEnumerable Views { get; set; } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentFriendsDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserFriendsDto.cs similarity index 77% rename from MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentFriendsDto.cs rename to MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserFriendsDto.cs index f22288d5b..90765c36d 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/StudentFriendsDto.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserFriendsDto.cs @@ -5,9 +5,9 @@ namespace MiniSpace.Services.Events.Application.DTO { [ExcludeFromCodeCoverage] - public class StudentFriendsDto + public class UserFriendsDto { - public Guid StudentId { get; set; } + public Guid UserId { get; set; } public List Friends { get; set; } = new List(); } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserFromServiceDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserFromServiceDto.cs new file mode 100644 index 000000000..55706a5ae --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/UserFromServiceDto.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + + +namespace MiniSpace.Services.Events.Application.DTO +{ + [ExcludeFromCodeCoverage] + public class UserFromServiceDto + { + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string ProfileImageUrl { get; set; } + public string Description { get; set; } + public DateTime? DateOfBirth { get; set; } + public bool EmailNotifications { get; set; } + public bool IsBanned { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public string ContactEmail { get; set; } + public string BannerUrl { get; set; } + public string PhoneNumber { get; set; } + public IEnumerable Languages { get; set; } + public IEnumerable Interests { get; set; } + public IEnumerable Education { get; set; } + public IEnumerable Work { get; set; } + public bool IsTwoFactorEnabled { get; set; } + public string TwoFactorSecret { get; set; } + public IEnumerable InterestedInEvents { get; set; } + public IEnumerable SignedUpEvents { get; set; } + public string Country { get; set; } + public string City { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/ViewDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/ViewDto.cs new file mode 100644 index 000000000..618f4a79c --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/ViewDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Events.Application.DTO +{ + public class ViewDto + { + public Guid EventId { get; set; } + public DateTime Date { get; set; } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/WorkDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/WorkDto.cs new file mode 100644 index 000000000..77b848b25 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/WorkDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Events.Application.DTO +{ + [ExcludeFromCodeCoverage] + public class WorkDto + { + public string Company { get; set; } + public string Position { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/CommentCreated.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/CommentCreated.cs new file mode 100644 index 000000000..521748421 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/CommentCreated.cs @@ -0,0 +1,37 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using System; + +namespace MiniSpace.Services.Events.Application.Events.External +{ + [Message("comments")] + public class CommentCreated : IEvent + { + public Guid CommentId { get; } + public Guid ContextId { get; } + public string CommentContext { get; } + public Guid UserId { get; } + public Guid ParentId { get; } + public string TextContent { get; } + public DateTime CreatedAt { get; } + public DateTime LastUpdatedAt { get; } + public int RepliesCount { get; } + public bool IsDeleted { get; } + + public CommentCreated(Guid commentId, Guid contextId, string commentContext, Guid userId, + Guid parentId, string textContent, DateTime createdAt, + DateTime lastUpdatedAt, int repliesCount, bool isDeleted) + { + CommentId = commentId; + ContextId = contextId; + CommentContext = commentContext; + UserId = userId; + ParentId = parentId; + TextContent = textContent; + CreatedAt = createdAt; + LastUpdatedAt = lastUpdatedAt; + RepliesCount = repliesCount; + IsDeleted = isDeleted; + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/CommentCreatedHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/CommentCreatedHandler.cs new file mode 100644 index 000000000..326facb80 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/CommentCreatedHandler.cs @@ -0,0 +1,47 @@ +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using MiniSpace.Services.Events.Application.Events.External; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.Application.Events.External.Handlers +{ + public class CommentCreatedHandler : IEventHandler + { + private readonly IUserCommentsHistoryRepository _userCommentsHistoryRepository; + + public CommentCreatedHandler(IUserCommentsHistoryRepository userCommentsHistoryRepository) + { + _userCommentsHistoryRepository = userCommentsHistoryRepository; + } + + public async Task HandleAsync(CommentCreated @event, CancellationToken cancellationToken = default) + { + + var eventJson = JsonSerializer.Serialize(@event, new JsonSerializerOptions + { + WriteIndented = true // Optional: For pretty-printing the JSON + }); + Console.WriteLine("Received CommentCreated event:"); + Console.WriteLine(eventJson); + + var comment = new Comment( + @event.CommentId, + @event.ContextId, + @event.CommentContext, + @event.UserId, + @event.ParentId, + @event.TextContent, + @event.CreatedAt, + @event.LastUpdatedAt, + @event.RepliesCount, + @event.IsDeleted + ); + + await _userCommentsHistoryRepository.SaveCommentAsync(@event.UserId, comment); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/ReactionCreatedHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/ReactionCreatedHandler.cs new file mode 100644 index 000000000..e4967cbf3 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/ReactionCreatedHandler.cs @@ -0,0 +1,42 @@ +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using MiniSpace.Services.Events.Application.Events.External; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.Application.Events.External.Handlers +{ + public class ReactionCreatedHandler : IEventHandler + { + private readonly IUserReactionsHistoryRepository _userReactionsHistoryRepository; + + public ReactionCreatedHandler(IUserReactionsHistoryRepository userReactionsHistoryRepository) + { + _userReactionsHistoryRepository = userReactionsHistoryRepository; + } + + public async Task HandleAsync(ReactionCreated @event, CancellationToken cancellationToken = default) + { + var eventJson = JsonSerializer.Serialize(@event, new JsonSerializerOptions + { + WriteIndented = true + }); + Console.WriteLine("Received ReactionCreated event:"); + Console.WriteLine(eventJson); + + var reaction = Reaction.Create( + @event.ReactionId, + @event.UserId, + @event.ReactionType, + @event.ContentId, + @event.ContentType, + @event.TargetType + ); + + await _userReactionsHistoryRepository.SaveReactionAsync(@event.UserId, reaction); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/ReactionCreated.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/ReactionCreated.cs new file mode 100644 index 000000000..0b4b0dfa1 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/ReactionCreated.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Events.Application.Events.External +{ + [Message("reactions")] + public class ReactionCreated : IEvent + { + public Guid ReactionId { get; } + public Guid UserId { get; } + public Guid ContentId { get; } + public string ContentType { get; } + public string ReactionType { get; } + public string TargetType { get; } + + public ReactionCreated(Guid reactionId, Guid userId, Guid contentId, string contentType, string reactionType, string targetType) + { + ReactionId = reactionId; + UserId = userId; + ContentId = contentId; + ContentType = contentType; + ReactionType = reactionType; + TargetType = targetType; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedEvents.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedEvents.cs index 84b4ae86c..03114a2cf 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedEvents.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedEvents.cs @@ -1,10 +1,11 @@ using Convey.CQRS.Queries; using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Wrappers; using System.Collections.Generic; namespace MiniSpace.Services.Events.Application.Queries { - public class GetPaginatedEvents : IQuery> + public class GetPaginatedEvents : IQuery> { public int Page { get; set; } = 1; public int PageSize { get; set; } = 10; diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedOrganizerEvents.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedOrganizerEvents.cs index 6cf1e1bcf..c87907342 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedOrganizerEvents.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedOrganizerEvents.cs @@ -1,10 +1,11 @@ using System; using Convey.CQRS.Queries; using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Wrappers; namespace MiniSpace.Services.Events.Application.Queries { - public class GetPaginatedOrganizerEvents : IQuery> + public class GetPaginatedOrganizerEvents : IQuery> { public Guid OrganizerId { get; set; } public int Page { get; set; } = 1; diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedUserViews.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedUserViews.cs new file mode 100644 index 000000000..4acc765f2 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetPaginatedUserViews.cs @@ -0,0 +1,14 @@ +using System; +using Convey.CQRS.Queries; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Wrappers; + +namespace MiniSpace.Services.Events.Application.Queries +{ + public class GetPaginatedUserViews : IQuery> + { + public Guid UserId { get; set; } + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 10; + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetSearchEvents.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetSearchEvents.cs index 64d2e61a6..50cbf6577 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetSearchEvents.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetSearchEvents.cs @@ -2,10 +2,11 @@ using System.Collections.Generic; using Convey.CQRS.Queries; using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Wrappers; namespace MiniSpace.Services.Events.Application.Queries { - public class GetSearchEvents : IQuery> + public class GetSearchEvents : IQuery> { public string Name { get; set; } public string Organizer { get; set; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetUserEvents.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetUserEvents.cs index 9d9dfe8cd..a24bdf88a 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetUserEvents.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetUserEvents.cs @@ -3,12 +3,11 @@ using System.Diagnostics.CodeAnalysis; using Convey.CQRS.Queries; using MiniSpace.Services.Events.Application.DTO; -using MiniSpace.Services.Events.Application.Wrappers; - +using MiniSpace.Services.Events.Core.Wrappers; namespace MiniSpace.Services.Events.Application.Queries { [ExcludeFromCodeCoverage] - public class GetUserEvents : IQuery> + public class GetUserEvents : IQuery> { public Guid UserId { get; set; } public string EngagementType { get; set; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetUserEventsFeed.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetUserEventsFeed.cs new file mode 100644 index 000000000..16b8bd6da --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Queries/GetUserEventsFeed.cs @@ -0,0 +1,28 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Convey.CQRS.Queries; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Wrappers; + +namespace MiniSpace.Services.Events.Application.Queries +{ + [ExcludeFromCodeCoverage] + public class GetUserEventsFeed : IQuery> + { + public Guid UserId { get; set; } + public int PageNumber { get; set; } = 1; + public int PageSize { get; set; } = 10; + public string SortBy { get; set; } = "PublishDate"; + public string Direction { get; set; } = "asc"; + + public GetUserEventsFeed(Guid userId, int pageNumber = 1, int pageSize = 10, + string sortBy = "PublishDate", string direction = "asc") + { + UserId = userId; + PageNumber = pageNumber; + PageSize = pageSize; + SortBy = sortBy; + Direction = direction; + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IFriendsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IFriendsServiceClient.cs index 605483c2d..07357399d 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IFriendsServiceClient.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IFriendsServiceClient.cs @@ -7,6 +7,6 @@ namespace MiniSpace.Services.Events.Application.Services.Clients { public interface IFriendsServiceClient { - Task> GetAsync(Guid studentId); + Task> GetAsync(Guid studentId); } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IStudentsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IStudentsServiceClient.cs index 1ed110d82..11885df3b 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IStudentsServiceClient.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/Clients/IStudentsServiceClient.cs @@ -8,5 +8,6 @@ public interface IStudentsServiceClient { Task GetAsync(Guid id); Task StudentExistsAsync(Guid id); + Task GetStudentByIdAsync(Guid studentId); } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventRecommendationService.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventRecommendationService.cs new file mode 100644 index 000000000..4a4d19586 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventRecommendationService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Application.DTO; + +namespace MiniSpace.Services.Events.Application.Services +{ + public interface IEventRecommendationService + { + IEnumerable RankEventsByUserInterest(Guid userId, IEnumerable events, IEnumerable userInterests); + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventService.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventService.cs index 7a3a9429c..486773f1e 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventService.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventService.cs @@ -2,13 +2,13 @@ using System.Threading.Tasks; using MiniSpace.Services.Events.Application.Commands; using MiniSpace.Services.Events.Application.DTO; -using MiniSpace.Services.Events.Application.Wrappers; +using MiniSpace.Services.Events.Core.Wrappers; namespace MiniSpace.Services.Events.Application.Services { public interface IEventService { - Task>> BrowseEventsAsync(SearchEvents command); - Task>> BrowseOrganizerEventsAsync(SearchOrganizerEvents command); + Task> BrowseEventsAsync(SearchEvents command); + Task> BrowseOrganizerEventsAsync(SearchOrganizerEvents command); } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Comment.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Comment.cs new file mode 100644 index 000000000..380d20d50 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Comment.cs @@ -0,0 +1,59 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Entities +{ + public class Comment + { + public Guid Id { get; private set; } + public Guid ContextId { get; private set; } + public string CommentContext { get; private set; } + public Guid UserId { get; private set; } + public Guid ParentId { get; private set; } + public string TextContent { get; private set; } + public DateTime CreatedAt { get; private set; } + public DateTime LastUpdatedAt { get; private set; } + public int RepliesCount { get; private set; } + public bool IsDeleted { get; private set; } + + public Comment(Guid id, Guid contextId, string commentContext, Guid userId, + Guid parentId, string textContent, DateTime createdAt, + DateTime lastUpdatedAt, int repliesCount, bool isDeleted) + { + Id = id; + ContextId = contextId; + CommentContext = commentContext; + UserId = userId; + ParentId = parentId; + TextContent = textContent; + CreatedAt = createdAt; + LastUpdatedAt = lastUpdatedAt; + RepliesCount = repliesCount; + IsDeleted = isDeleted; + } + + public void UpdateText(string newText, DateTime updatedAt) + { + TextContent = newText; + LastUpdatedAt = updatedAt; + } + + public void MarkAsDeleted() + { + IsDeleted = true; + TextContent = "[deleted]"; + } + + public void IncrementRepliesCount() + { + RepliesCount++; + } + + public void DecrementRepliesCount() + { + if (RepliesCount > 0) + { + RepliesCount--; + } + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventSettings.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventSettings.cs index a74438ae4..50994b963 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventSettings.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventSettings.cs @@ -19,9 +19,9 @@ public class EventSettings public bool AllowComments { get; set; } public bool RequiresPayment { get; set; } - public PaymentMethod PaymentMethod { get; set; } // Specifies if payment is online or offline - public string PaymentReceiverDetails { get; set; } // Details of the payment receiver (e.g., account info) - public string PaymentGateway { get; set; } // Specifies the payment gateway (e.g., Stripe) + public PaymentMethod PaymentMethod { get; set; } + public string PaymentReceiverDetails { get; set; } + public string PaymentGateway { get; set; } public bool IssueTickets { get; set; } public int MaxTicketsPerPerson { get; set; } public decimal TicketPrice { get; set; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventsViews.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventsViews.cs new file mode 100644 index 000000000..cdb7eb9a7 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/EventsViews.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Events.Core.Entities +{ + public class EventsViews + { + public Guid UserId { get; private set; } + public IEnumerable Views { get; private set; } + + public EventsViews(Guid userId, IEnumerable views) + { + UserId = userId; + Views = views ?? new List(); + } + + public void AddView(Guid eventId, DateTime date) + { + var viewList = new List(Views) + { + new View(eventId, date) + }; + Views = viewList; + } + + public void RemoveView(Guid eventId) + { + var viewList = new List(Views); + var viewToRemove = viewList.Find(view => view.EventId == eventId); + if (viewToRemove != null) + { + viewList.Remove(viewToRemove); + Views = viewList; + } + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Reaction.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Reaction.cs new file mode 100644 index 000000000..cd19619f5 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Reaction.cs @@ -0,0 +1,58 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Entities +{ + public class Reaction + { + public Guid Id { get; private set; } + public Guid UserId { get; private set; } + public Guid ContentId { get; private set; } + public string Type { get; private set; } + public string ContentType { get; private set; } + public string TargetType { get; private set; } + public DateTime CreatedAt { get; private set; } + + private Reaction() { } + + private Reaction(Guid id, Guid userId, string type, Guid contentId, string contentType, string targetType) + { + if (string.IsNullOrWhiteSpace(type)) + { + throw new ArgumentException("Reaction type cannot be null or empty.", nameof(type)); + } + + if (string.IsNullOrWhiteSpace(contentType)) + { + throw new ArgumentException("Content type cannot be null or empty.", nameof(contentType)); + } + + if (string.IsNullOrWhiteSpace(targetType)) + { + throw new ArgumentException("Target type cannot be null or empty.", nameof(targetType)); + } + + Id = id != Guid.Empty ? id : throw new ArgumentException("Reaction ID cannot be empty.", nameof(id)); + UserId = userId != Guid.Empty ? userId : throw new ArgumentException("User ID cannot be empty.", nameof(userId)); + ContentId = contentId != Guid.Empty ? contentId : throw new ArgumentException("Content ID cannot be empty.", nameof(contentId)); + Type = type; + ContentType = contentType; + TargetType = targetType; + CreatedAt = DateTime.UtcNow; + } + + public static Reaction Create(Guid id, Guid userId, string type, Guid contentId, string contentType, string targetType) + { + return new Reaction(id, userId, type, contentId, contentType, targetType); + } + + public void UpdateReactionType(string newType) + { + if (string.IsNullOrWhiteSpace(newType)) + { + throw new ArgumentException("Reaction type cannot be null or empty.", nameof(newType)); + } + + Type = newType; + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/View.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/View.cs new file mode 100644 index 000000000..9372d3fd5 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/View.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Events.Core.Entities +{ + public class View + { + public Guid EventId { get; private set; } + public DateTime Date { get; private set; } + + public View(Guid eventId, DateTime date) + { + EventId = eventId; + Date = date; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventsUserViewsRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventsUserViewsRepository.cs new file mode 100644 index 000000000..ccf4a7c9d --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventsUserViewsRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Core.Repositories +{ + public interface IEventsUserViewsRepository + { + Task GetAsync(Guid userId); + Task AddAsync(EventsViews eventsViews); + Task UpdateAsync(EventsViews eventsViews); + Task DeleteAsync(Guid userId); + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IUserCommentsHistoryRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IUserCommentsHistoryRepository.cs new file mode 100644 index 000000000..b6a50d66b --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IUserCommentsHistoryRepository.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Wrappers; + +namespace MiniSpace.Services.Events.Core.Repositories +{ + public interface IUserCommentsHistoryRepository + { + Task SaveCommentAsync(Guid userId, Comment comment); + + Task> GetUserCommentsAsync(Guid userId); + + Task> GetUserCommentsPagedAsync(Guid userId, int pageNumber, int pageSize); + + Task DeleteCommentAsync(Guid userId, Guid commentId); + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IUserReactionsHistoryRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IUserReactionsHistoryRepository.cs new file mode 100644 index 000000000..945cb1e3d --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IUserReactionsHistoryRepository.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Wrappers; + +namespace MiniSpace.Services.Events.Core.Repositories +{ + public interface IUserReactionsHistoryRepository + { + Task SaveReactionAsync(Guid userId, Reaction reaction); + + Task> GetUserReactionsAsync(Guid userId); + + Task> GetUserReactionsPagedAsync(Guid userId, int pageNumber, int pageSize); + + Task DeleteReactionAsync(Guid userId, Guid reactionId); + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Wrappers/PagedResponse.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Wrappers/PagedResponse.cs index 4f5edc17f..017a5dbe6 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Wrappers/PagedResponse.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Wrappers/PagedResponse.cs @@ -1,31 +1,35 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; -namespace MiniSpace.Services.Events.Application.Wrappers +namespace MiniSpace.Services.Events.Core.Wrappers { [ExcludeFromCodeCoverage] - public class PagedResponse : Response + public class PagedResponse { + public IEnumerable Items { get; } public int TotalPages { get; } - public int TotalElements { get; } - public int Size { get; } - public int Number { get; } + public int TotalItems { get; } + public int PageSize { get; } + public int Page { get; } public bool First { get; } public bool Last { get; } public bool Empty { get; } + public int? NextPage => Page < TotalPages ? Page + 1 : (int?)null; + public int? PreviousPage => Page > 1 ? Page - 1 : (int?)null; - public PagedResponse(T content, int pageNumber, int pageSize, int totalPages, int totalElements) + public PagedResponse(IEnumerable items, int page, int pageSize, int totalItems) { - Content = content; - TotalPages = totalPages; - TotalElements = totalElements; - Size = pageSize; - Number = pageNumber; - First = pageNumber == 0; - Last = pageNumber == totalPages - 1; - Empty = totalElements == 0; - Succeeded = true; - Errors = null; - Message = null; + Items = items; + PageSize = pageSize; + TotalItems = totalItems; + TotalPages = pageSize > 0 ? (int)Math.Ceiling((decimal)totalItems / pageSize) : 0; + Page = page; + First = page == 1; + Last = page == TotalPages; + Empty = !items.Any(); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs index e84ef6592..0dd14dcdb 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs @@ -49,6 +49,9 @@ using MiniSpace.Services.Events.Infrastructure.Services.Clients; using MiniSpace.Services.Events.Infrastructure.Services.Workers; using System.Diagnostics.CodeAnalysis; +using MiniSpace.Services.Posts.Infrastructure.Mongo.Repositories; +using MiniSpace.Services.Events.Infrastructure.Services.Recommendation; +using Microsoft.ML; namespace MiniSpace.Services.Events.Infrastructure { @@ -61,15 +64,22 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(ctx => ctx.GetRequiredService().Create()); builder.Services.TryDecorate(typeof(ICommandHandler<>), typeof(OutboxCommandHandlerDecorator<>)); builder.Services.TryDecorate(typeof(IEventHandler<>), typeof(OutboxEventHandlerDecorator<>)); + builder.Services.AddSingleton(); builder.Services.AddHostedService(); return builder @@ -88,6 +98,9 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) .AddJaeger() .AddHandlersLogging() .AddMongoRepository("events") + .AddMongoRepository("events_views") + .AddMongoRepository("user_comments_history") + .AddMongoRepository("events") .AddWebApiSwaggerDocs() .AddSecurity(); } @@ -113,7 +126,9 @@ public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app .SubscribeCommand() .SubscribeCommand() .SubscribeEvent() - .SubscribeEvent(); + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent(); return app; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/MiniSpace.Services.Events.Infrastructure.csproj b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/MiniSpace.Services.Events.Infrastructure.csproj index af01e2591..92b3b024b 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/MiniSpace.Services.Events.Infrastructure.csproj +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/MiniSpace.Services.Events.Infrastructure.csproj @@ -29,6 +29,10 @@ + + + + diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/CommentDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/CommentDocument.cs new file mode 100644 index 000000000..1e8a23bdd --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/CommentDocument.cs @@ -0,0 +1,19 @@ +using System; +using Convey.Types; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public class CommentDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid ContextId { get; set; } + public string CommentContext { get; set; } + public Guid UserId { get; set; } + public Guid ParentId { get; set; } + public string TextContent { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime LastUpdatedAt { get; set; } + public int RepliesCount { get; set; } + public bool IsDeleted { get; set; } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventsViewsExtensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventsViewsExtensions.cs new file mode 100644 index 000000000..a4d4052e8 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventsViewsExtensions.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public static class EventsViewsExtensions + { + public static UserEventsViewsDto AsDto(this UserEventsViewsDocument document) + { + return new UserEventsViewsDto + { + UserId = document.UserId, + Views = document.Views.Select(v => new ViewDto + { + EventId = v.EventId, + Date = v.Date + }) + }; + } + + public static UserEventsViewsDocument AsDocument(this EventsViews entity) + { + return new UserEventsViewsDocument + { + Id = Guid.NewGuid(), + UserId = entity.UserId, + Views = entity.Views.Select(ViewDocument.FromEntity).ToList() + }; + } + + public static EventsViews AsEntity(this UserEventsViewsDocument document) + { + return new EventsViews( + document.UserId, + document.Views.Select(v => new View(v.EventId, v.Date)) + ); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs index d2abe39f9..f1915a172 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs @@ -46,11 +46,11 @@ public static EventDto AsDto(this Event @event, Guid studentId) Id = @event.Id, Name = @event.Name, Description = @event.Description, - Organizer = @event.Organizer.AsDto(), // Assuming an AsDto method exists for Organizer + Organizer = @event.Organizer.AsDto(), StartDate = @event.StartDate, EndDate = @event.EndDate, - Location = @event.Location.AsDto(), // Assuming an AsDto method exists for Address - MediaFilesUrl = @event.MediaFiles?.ToList(), // Converting MediaFiles to a list of URLs + Location = @event.Location.AsDto(), + MediaFilesUrl = @event.MediaFiles?.ToList(), BannerUrl = @event.BannerUrl, InterestedStudents = @event.InterestedParticipants.Count(), SignedUpStudents = @event.SignedUpParticipants.Count(), @@ -61,12 +61,12 @@ public static EventDto AsDto(this Event @event, Guid studentId) PublishDate = @event.PublishDate, UpdatedAt = @event.UpdatedAt, Visibility = @event.Visibility, - Settings = new EventSettingsDto(@event.Settings), // Assuming an AsDto method exists for EventSettings + Settings = new EventSettingsDto(@event.Settings), IsSignedUp = @event.SignedUpParticipants.Any(x => x.StudentId == studentId), IsInterested = @event.InterestedParticipants.Any(x => x.StudentId == studentId), StudentRating = @event.Ratings.FirstOrDefault(x => x.StudentId == studentId)?.Value, - FriendsInterestedIn = Enumerable.Empty(), // Placeholder, customize as needed - FriendsSignedUp = Enumerable.Empty() // Placeholder, customize as needed + FriendsInterestedIn = Enumerable.Empty(), + FriendsSignedUp = Enumerable.Empty() }; } @@ -240,5 +240,95 @@ public static RatingDto AsRatingDto(this RatingDocument document) public static Rating AsEntity(this RatingDocument document) => new (document.StudentId, document.Value); + + + public static CommentDocument AsDocument(this Comment comment) + { + return new CommentDocument + { + Id = comment.Id, + ContextId = comment.ContextId, + CommentContext = comment.CommentContext, + UserId = comment.UserId, + ParentId = comment.ParentId, + TextContent = comment.TextContent, + CreatedAt = comment.CreatedAt, + LastUpdatedAt = comment.LastUpdatedAt, + RepliesCount = comment.RepliesCount, + IsDeleted = comment.IsDeleted + }; + } + + public static Comment AsEntity(this CommentDocument document) + { + return new Comment( + document.Id, + document.ContextId, + document.CommentContext, + document.UserId, + document.ParentId, + document.TextContent, + document.CreatedAt, + document.LastUpdatedAt, + document.RepliesCount, + document.IsDeleted + ); + } + + public static UserCommentsDocument AsDocument(this IEnumerable comments, Guid userId) + { + return new UserCommentsDocument + { + Id = Guid.NewGuid(), + UserId = userId, + Comments = comments.Select(comment => comment.AsDocument()).ToList() + }; + } + + public static IEnumerable AsEntities(this UserCommentsDocument document) + { + return document.Comments.Select(doc => doc.AsEntity()); + } + + public static ReactionDocument AsDocument(this Reaction reaction) + { + return new ReactionDocument + { + Id = reaction.Id, + UserId = reaction.UserId, + ContentId = reaction.ContentId, + ContentType = reaction.ContentType, + ReactionType = reaction.Type, + TargetType = reaction.TargetType, + CreatedAt = reaction.CreatedAt + }; + } + + public static Reaction AsEntity(this ReactionDocument document) + { + return Reaction.Create( + document.Id, + document.UserId, + document.ReactionType, + document.ContentId, + document.ContentType, + document.TargetType + ); + } + + public static UserReactionDocument AsDocument(this IEnumerable reactions, Guid userId) + { + return new UserReactionDocument + { + Id = Guid.NewGuid(), + UserId = userId, + Reactions = reactions.Select(reaction => reaction.AsDocument()).ToList() + }; + } + + public static IEnumerable AsEntities(this UserReactionDocument document) + { + return document.Reactions.Select(doc => doc.AsEntity()); + } } } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/ReactionDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/ReactionDocument.cs new file mode 100644 index 000000000..321470768 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/ReactionDocument.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public class ReactionDocument + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + public Guid ContentId { get; set; } + public string ContentType { get; set; } + public string ReactionType { get; set; } + public string TargetType { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserCommentsDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserCommentsDocument.cs new file mode 100644 index 000000000..793024de0 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserCommentsDocument.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using Convey.Types; +using MongoDB.Bson.Serialization.Attributes; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public class UserCommentsDocument : IIdentifiable + { + [BsonId] + [BsonRepresentation(MongoDB.Bson.BsonType.String)] + public Guid Id { get; set; } + public Guid UserId { get; set; } + public IEnumerable Comments { get; set; } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserEventsViewsDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserEventsViewsDocument.cs new file mode 100644 index 000000000..cffe5a1ec --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserEventsViewsDocument.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Convey.Types; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public class UserEventsViewsDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + public List Views { get; set; } = new List(); + + public static UserEventsViewsDocument FromEntity(EventsViews eventsViews) + { + return new UserEventsViewsDocument + { + Id = Guid.NewGuid(), + UserId = eventsViews.UserId, + Views = new List(eventsViews.Views.Select(ViewDocument.FromEntity)) + }; + } + + public EventsViews ToEntity() + { + return new EventsViews(UserId, Views.Select(view => view.ToEntity())); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserReactionDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserReactionDocument.cs new file mode 100644 index 000000000..6549c7b7a --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/UserReactionDocument.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using Convey.Types; +using MongoDB.Bson.Serialization.Attributes; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public class UserReactionDocument : IIdentifiable + { + [BsonId] + [BsonRepresentation(MongoDB.Bson.BsonType.String)] + public Guid Id { get; set; } + public Guid UserId { get; set; } + public IEnumerable Reactions { get; set; } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/ViewDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/ViewDocument.cs new file mode 100644 index 000000000..74a3f40ee --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/ViewDocument.cs @@ -0,0 +1,28 @@ +using System; +using Convey.Types; +using MiniSpace.Services.Events.Core.Entities; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Documents +{ + public class ViewDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid EventId { get; set; } + public DateTime Date { get; set; } + + public static ViewDocument FromEntity(View view) + { + return new ViewDocument + { + Id = Guid.NewGuid(), + EventId = view.EventId, + Date = view.Date + }; + } + + public View ToEntity() + { + return new View(EventId, Date); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventHandler.cs index 40f499e3e..678331a12 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventHandler.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetEventHandler.cs @@ -36,24 +36,33 @@ public GetEventHandler(IMongoRepository eventRepository, public async Task HandleAsync(GetEvent query, CancellationToken cancellationToken) { var document = await _eventRepository.GetAsync(p => p.Id == query.EventId); - if(document is null) + if (document == null) { return null; } + var identity = _appContext.Identity; var friends = Enumerable.Empty(); - if(identity.IsAuthenticated) + + if (identity.IsAuthenticated) { - var result = await _friendsServiceClient.GetAsync(identity.Id); - if (result != null && result.Any()) + try + { + var userFriends = await _friendsServiceClient.GetAsync(identity.Id); + if (userFriends != null && userFriends.Any()) + { + friends = userFriends.SelectMany(uf => uf.Friends); + } + } + catch (Exception ex) { - friends = result.First().Friends; + Console.Error.WriteLine($"Error fetching friends: {ex.Message}"); + throw new ApplicationException("An error occurred while fetching friends data.", ex); } } await _messageBroker.PublishAsync(new EventViewed(query.EventId)); return document.AsDtoWithFriends(identity.Id, friends); } - } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedEventsHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedEventsHandler.cs index 537194953..a56403972 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedEventsHandler.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedEventsHandler.cs @@ -7,11 +7,12 @@ using MiniSpace.Services.Events.Application.DTO; using MiniSpace.Services.Events.Application.Queries; using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; namespace MiniSpace.Services.Events.Infrastructure.Mongo.Queries.Handlers { - public class GetPaginatedEventsHandler : IQueryHandler> + public class GetPaginatedEventsHandler : IQueryHandler> { private readonly IEventRepository _eventRepository; private readonly IAppContext _appContext; @@ -22,30 +23,28 @@ public GetPaginatedEventsHandler(IEventRepository eventRepository, IAppContext a _appContext = appContext; } - public async Task> HandleAsync(GetPaginatedEvents query, CancellationToken cancellationToken) + public async Task> HandleAsync(GetPaginatedEvents query, CancellationToken cancellationToken) { - // Fetch the paginated events from the repository var (events, pageNumber, pageSize, totalPages, totalElements) = await _eventRepository.BrowseEventsAsync( pageNumber: query.Page, pageSize: query.PageSize, - name: string.Empty, // Assuming no filtering by name - organizer: string.Empty, // Assuming no filtering by organizer - dateFrom: default, // Assuming no date filter - dateTo: default, // Assuming no date filter - category: null, // Assuming no category filter - state: null, // Assuming no state filter - organizations: Enumerable.Empty(), // Assuming no organization filter - friends: Enumerable.Empty(), // Assuming no friends filter - friendsEngagementType: null, // Assuming no engagement type filter - sortBy: Enumerable.Empty(), // Assuming no sorting - direction: string.Empty // Assuming no sorting direction + name: string.Empty, + organizer: string.Empty, + dateFrom: default, + dateTo: default, + category: null, + state: null, + organizations: Enumerable.Empty(), + friends: Enumerable.Empty(), + friendsEngagementType: null, + sortBy: Enumerable.Empty(), + direction: string.Empty ); var studentId = _appContext.Identity.Id; var eventDtos = events.Select(e => e.AsDto(studentId)).ToList(); - // Return a paged result with the fetched data - return new MiniSpace.Services.Events.Application.DTO.PagedResult(eventDtos, pageNumber, pageSize, totalElements); + return new PagedResponse(eventDtos, pageNumber, pageSize, totalElements); } } } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizerEventsHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizerEventsHandler.cs index 1499d4630..242872cb4 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizerEventsHandler.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizerEventsHandler.cs @@ -7,11 +7,12 @@ using MiniSpace.Services.Events.Application.DTO; using MiniSpace.Services.Events.Application.Queries; using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; namespace MiniSpace.Services.Events.Infrastructure.Mongo.Queries.Handlers { - public class GetPaginatedOrganizerEventsHandler : IQueryHandler> + public class GetPaginatedOrganizerEventsHandler : IQueryHandler> { private readonly IEventRepository _eventRepository; private readonly IAppContext _appContext; @@ -22,7 +23,7 @@ public GetPaginatedOrganizerEventsHandler(IEventRepository eventRepository, IApp _appContext = appContext; } - public async Task> HandleAsync(GetPaginatedOrganizerEvents query, CancellationToken cancellationToken) + public async Task> HandleAsync(GetPaginatedOrganizerEvents query, CancellationToken cancellationToken) { var (events, pageNumber, pageSize, totalPages, totalElements) = await _eventRepository.BrowseOrganizerEventsAsync( pageNumber: query.Page, @@ -39,7 +40,7 @@ public GetPaginatedOrganizerEventsHandler(IEventRepository eventRepository, IApp var studentId = _appContext.Identity.Id; var eventDtos = events.Select(e => e.AsDto(studentId)).ToList(); - return new MiniSpace.Services.Events.Application.DTO.PagedResult(eventDtos, pageNumber, pageSize, totalElements); + return new PagedResponse(eventDtos, pageNumber, pageSize, totalElements); } } } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedSearchEventsHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedSearchEventsHandler.cs index 48c12954e..14bffc25e 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedSearchEventsHandler.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedSearchEventsHandler.cs @@ -10,11 +10,12 @@ using MiniSpace.Services.Events.Application.Queries; using MiniSpace.Services.Events.Core.Entities; using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; namespace MiniSpace.Services.Events.Infrastructure.Mongo.Queries.Handlers { - public class GetPaginatedSearchEventsHandler : IQueryHandler> + public class GetPaginatedSearchEventsHandler : IQueryHandler> { private readonly IEventRepository _eventRepository; private readonly IAppContext _appContext; @@ -25,14 +26,13 @@ public GetPaginatedSearchEventsHandler(IEventRepository eventRepository, IAppCon _appContext = appContext; } - public async Task> HandleAsync(GetSearchEvents query, CancellationToken cancellationToken) + public async Task> HandleAsync(GetSearchEvents query, CancellationToken cancellationToken) { - var jsonOptionsx = new JsonSerializerOptions { WriteIndented = true }; + var jsonOptionsx = new JsonSerializerOptions { WriteIndented = true }; var queryJson = JsonSerializer.Serialize(query, jsonOptionsx); Console.WriteLine("Query Object: "); Console.WriteLine(queryJson); - // Convert string values to corresponding enum types Category? category = null; State? state = null; EventEngagementType? engagementType = null; @@ -52,15 +52,12 @@ public GetPaginatedSearchEventsHandler(IEventRepository eventRepository, IAppCon engagementType = parsedEngagementType; } - // Use PageableDto for pagination and sorting var pageNumber = query.Pageable?.Page ?? 1; var pageSize = query.Pageable?.Size ?? 10; - // Handle sorting var sortBy = query.Pageable?.Sort?.SortBy ?? Enumerable.Empty(); var sortDirection = query.Pageable?.Sort?.Direction ?? string.Empty; - // Fetch the events based on the query parameters var (events, returnedPageNumber, returnedPageSize, totalPages, totalElements) = await _eventRepository.BrowseEventsAsync( pageNumber: pageNumber, pageSize: pageSize, @@ -77,19 +74,16 @@ public GetPaginatedSearchEventsHandler(IEventRepository eventRepository, IAppCon direction: sortDirection ); - // Map events to DTOs var studentId = _appContext.Identity.Id; var eventDtos = events.Select(e => e.AsDto(studentId)).ToList(); - var pagedResult = new MiniSpace.Services.Events.Application.DTO.PagedResult(eventDtos, pageNumber, pageSize, totalElements); + var pagedResult = new PagedResponse(eventDtos, pageNumber, pageSize, totalElements); - // Serialize the result to JSON and log it var jsonOptions = new JsonSerializerOptions { WriteIndented = true }; var jsonResult = JsonSerializer.Serialize(pagedResult, jsonOptions); Console.WriteLine("Search Results: "); Console.WriteLine(jsonResult); - // Return the paginated result return pagedResult; } } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedUserViewsHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedUserViewsHandler.cs new file mode 100644 index 000000000..06d9d7d90 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetPaginatedUserViewsHandler.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; + +namespace MiniSpace.Services.Events.Application.Queries.Handlers +{ + public class GetPaginatedUserViewsHandler : IQueryHandler> + { + private readonly IEventsUserViewsRepository _eventsUserViewsRepository; + + public GetPaginatedUserViewsHandler(IEventsUserViewsRepository eventsUserViewsRepository) + { + _eventsUserViewsRepository = eventsUserViewsRepository; + } + + public async Task> HandleAsync(GetPaginatedUserViews query, CancellationToken cancellationToken) + { + var userViews = await _eventsUserViewsRepository.GetAsync(query.UserId); + + if (userViews == null) + { + return new PagedResponse(Enumerable.Empty(), query.Page, query.PageSize, 0); + } + + var totalItems = userViews.Views.Count(); + var views = userViews.Views + .Skip((query.Page - 1) * query.PageSize) + .Take(query.PageSize) + .Select(view => new ViewDto + { + EventId = view.EventId, + Date = view.Date + }) + .ToList(); + + return new PagedResponse(views, query.Page, query.PageSize, totalItems); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetUserEventsFeedHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetUserEventsFeedHandler.cs new file mode 100644 index 000000000..f980a48cd --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetUserEventsFeedHandler.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Application.Queries; +using MiniSpace.Services.Events.Application.Services; +using MiniSpace.Services.Events.Application.Services.Clients; +using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using MiniSpace.Services.Events.Application; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Events.Infrastructure.Queries.Handlers +{ + public class GetUserEventsFeedHandler : IQueryHandler> + { + private readonly IEventRepository _eventRepository; + private readonly IEventRecommendationService _eventRecommendationService; + private readonly IStudentsServiceClient _studentsServiceClient; + private readonly IAppContext _appContext; + private readonly ILogger _logger; + + public GetUserEventsFeedHandler( + IEventRepository eventRepository, + IEventRecommendationService eventRecommendationService, + IStudentsServiceClient studentsServiceClient, + IAppContext appContext, + ILogger logger) + { + _eventRepository = eventRepository; + _eventRecommendationService = eventRecommendationService; + _studentsServiceClient = studentsServiceClient; + _appContext = appContext; + _logger = logger; + } + + public async Task> HandleAsync(GetUserEventsFeed query, CancellationToken cancellationToken) + { + _logger.LogInformation("Handling GetUserEventsFeed query: {Query}", JsonConvert.SerializeObject(query)); + + // Fetch user data and events in parallel + var userTask = _studentsServiceClient.GetStudentByIdAsync(query.UserId); + var eventsTask = _eventRepository.BrowseEventsAsync( + query.PageNumber, + query.PageSize, + name: string.Empty, + organizer: string.Empty, + dateFrom: default, + dateTo: default, + category: null, + state: null, + organizations: Enumerable.Empty(), + friends: Enumerable.Empty(), + friendsEngagementType: null, + sortBy: new List { query.SortBy }, + direction: query.Direction + ); + + await Task.WhenAll(userTask, eventsTask); + + var user = userTask.Result; + var (events, _, _, _, _) = eventsTask.Result; + + if (user == null) + { + return new PagedResponse(Enumerable.Empty(), query.PageNumber, query.PageSize, 0); + } + + var studentId = _appContext.Identity.Id; + var eventDtos = events.Select(e => e.AsDto(studentId)).ToList(); + + // Rank events using the recommendation service + var rankedEvents = _eventRecommendationService.RankEventsByUserInterest(query.UserId, eventDtos, user.Interests); + + if (!rankedEvents.Any()) + { + // If no ranked events, fetch all events + _logger.LogInformation("No ranked events found, loading all events."); + var allEventsTask = _eventRepository.BrowseEventsAsync( + query.PageNumber, + query.PageSize, + name: string.Empty, + organizer: string.Empty, + dateFrom: default, + dateTo: default, + category: null, + state: null, + organizations: Enumerable.Empty(), + friends: Enumerable.Empty(), + friendsEngagementType: null, + sortBy: new List { query.SortBy }, + direction: query.Direction + ); + + await allEventsTask; + + var (allEvents, _, _, _, _) = allEventsTask.Result; + eventDtos = allEvents.Select(e => e.AsDto(studentId)).ToList(); + rankedEvents = eventDtos; // Use all events as the ranked events + } + + // Paginate the ranked events + var pagedEvents = rankedEvents + .Skip((query.PageNumber - 1) * query.PageSize) + .Take(query.PageSize) + .ToList(); + + var response = new PagedResponse(pagedEvents, query.PageNumber, query.PageSize, rankedEvents.Count()); + + // Log the response for debugging purposes + var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented); + _logger.LogInformation("Response JSON: {JsonResponse}", jsonResponse); + + return response; + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetUserEventsHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetUserEventsHandler.cs index 00e980776..b477b3c0c 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetUserEventsHandler.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Queries/Handlers/GetUserEventsHandler.cs @@ -11,12 +11,13 @@ using MiniSpace.Services.Events.Application.Services; using MiniSpace.Services.Events.Application.Services.Clients; using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Wrappers; using MiniSpace.Services.Events.Core.Repositories; namespace MiniSpace.Services.Events.Infrastructure.Mongo.Queries.Handlers { [ExcludeFromCodeCoverage] - public class GetUserEventsHandler : IQueryHandler> + public class GetUserEventsHandler : IQueryHandler> { private readonly IEventRepository _eventRepository; private readonly IStudentsServiceClient _studentsServiceClient; @@ -32,12 +33,12 @@ public GetUserEventsHandler(IEventRepository eventRepository, _appContext = appContext; } - public async Task> HandleAsync(GetUserEvents query, CancellationToken cancellationToken) + public async Task> HandleAsync(GetUserEvents query, CancellationToken cancellationToken) { var identity = _appContext.Identity; if (identity.IsAuthenticated && identity.Id != query.UserId) { - return new MiniSpace.Services.Events.Application.DTO.PagedResult(Enumerable.Empty(), 1, query.NumberOfResults, 0); + return new PagedResponse(Enumerable.Empty(), 1, query.NumberOfResults, 0); } int pageSize = query.NumberOfResults > 0 ? query.NumberOfResults : 10; @@ -55,7 +56,7 @@ public GetUserEventsHandler(IEventRepository eventRepository, var result = await _eventRepository.BrowseStudentEventsAsync(query.Page, pageSize, studentEventIds, Enumerable.Empty(), "asc"); - return new MiniSpace.Services.Events.Application.DTO.PagedResult( + return new PagedResponse( result.events.Select(e => new EventDto(e, identity.Id)), result.pageNumber, result.pageSize, diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventsUserViewsRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventsUserViewsRepository.cs new file mode 100644 index 000000000..615606031 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventsUserViewsRepository.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading.Tasks; +using MongoDB.Driver; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; +using Convey.Persistence.MongoDB; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Repositories +{ + public class EventsUserViewsRepository : IEventsUserViewsRepository + { + private readonly IMongoRepository _repository; + + public EventsUserViewsRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid userId) + { + var document = await _repository.GetAsync(x => x.UserId == userId); + return document?.ToEntity(); + } + + public async Task AddAsync(EventsViews eventsViews) + { + var document = eventsViews.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task UpdateAsync(EventsViews eventsViews) + { + var document = eventsViews.AsDocument(); + await _repository.UpdateAsync(document); + } + + public async Task DeleteAsync(Guid userId) + { + await _repository.DeleteAsync(x => x.UserId == userId); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/UserCommentsHistoryMongoRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/UserCommentsHistoryMongoRepository.cs new file mode 100644 index 000000000..beb750dc7 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/UserCommentsHistoryMongoRepository.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; +using MongoDB.Driver; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Repositories +{ + public class UserCommentsHistoryMongoRepository : IUserCommentsHistoryRepository + { + private readonly IMongoCollection _collection; + + public UserCommentsHistoryMongoRepository(IMongoDatabase database) + { + _collection = database.GetCollection("user_reactions_history"); + } + + public async Task SaveCommentAsync(Guid userId, Comment comment) + { + var filter = Builders.Filter.Eq(uc => uc.UserId, userId); + + var commentDocument = comment.AsDocument(); + + var update = Builders.Update.Combine( + Builders.Update.Push(uc => uc.Comments, commentDocument), + Builders.Update.SetOnInsert(uc => uc.UserId, userId), + Builders.Update.SetOnInsert(uc => uc.Id, Guid.NewGuid()) + ); + + await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + } + + public async Task> GetUserCommentsAsync(Guid userId) + { + var userCommentsDocument = await _collection.Find(uc => uc.UserId == userId).FirstOrDefaultAsync(); + return userCommentsDocument?.AsEntities() ?? Enumerable.Empty(); + } + + public async Task> GetUserCommentsPagedAsync(Guid userId, int pageNumber, int pageSize) + { + var userCommentsDocument = await _collection.Find(uc => uc.UserId == userId).FirstOrDefaultAsync(); + + if (userCommentsDocument == null) + { + return new PagedResponse(Enumerable.Empty(), pageNumber, pageSize, 0); + } + + var pagedComments = userCommentsDocument.Comments + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .Select(doc => doc.AsEntity()); + + return new PagedResponse(pagedComments, pageNumber, pageSize, userCommentsDocument.Comments.Count()); + } + + public async Task DeleteCommentAsync(Guid userId, Guid commentId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(uc => uc.UserId, userId), + Builders.Filter.ElemMatch(uc => uc.Comments, c => c.Id == commentId) + ); + + var update = Builders.Update.PullFilter(uc => uc.Comments, c => c.Id == commentId); + await _collection.UpdateOneAsync(filter, update); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/UserReactionsHistoryMongoRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/UserReactionsHistoryMongoRepository.cs new file mode 100644 index 000000000..fb6e7e739 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/UserReactionsHistoryMongoRepository.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MiniSpace.Services.Events.Core.Entities; +using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; +using MiniSpace.Services.Events.Infrastructure.Mongo.Documents; +using MongoDB.Driver; + + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Repositories +{ + public class UserReactionsHistoryMongoRepository : IUserReactionsHistoryRepository + { + private readonly IMongoCollection _collection; + + public UserReactionsHistoryMongoRepository(IMongoDatabase database) + { + _collection = database.GetCollection("user_reactions_history"); + } + + public async Task SaveReactionAsync(Guid userId, Reaction reaction) + { + var filter = Builders.Filter.Eq(ur => ur.UserId, userId); + + var reactionDocument = reaction.AsDocument(); + + var update = Builders.Update.Combine( + Builders.Update.Push(ur => ur.Reactions, reactionDocument), + Builders.Update.SetOnInsert(ur => ur.UserId, userId), + Builders.Update.SetOnInsert(ur => ur.Id, Guid.NewGuid()) + ); + + await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + } + + public async Task> GetUserReactionsAsync(Guid userId) + { + var userReactionsDocument = await _collection.Find(ur => ur.UserId == userId).FirstOrDefaultAsync(); + return userReactionsDocument?.AsEntities() ?? Enumerable.Empty(); + } + + public async Task> GetUserReactionsPagedAsync(Guid userId, int pageNumber, int pageSize) + { + var userReactionsDocument = await _collection.Find(ur => ur.UserId == userId).FirstOrDefaultAsync(); + + if (userReactionsDocument == null) + { + return new PagedResponse(Enumerable.Empty(), pageNumber, pageSize, 0); + } + + var pagedReactions = userReactionsDocument.Reactions + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .Select(doc => doc.AsEntity()); + + return new PagedResponse(pagedReactions, pageNumber, pageSize, userReactionsDocument.Reactions.Count()); + } + + public async Task DeleteReactionAsync(Guid userId, Guid reactionId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(ur => ur.UserId, userId), + Builders.Filter.ElemMatch(ur => ur.Reactions, r => r.Id == reactionId) + ); + + var update = Builders.Update.PullFilter(ur => ur.Reactions, r => r.Id == reactionId); + await _collection.UpdateOneAsync(filter, update); + } + } +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/FriendsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/FriendsServiceClient.cs index 601086ac5..89f5205ae 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/FriendsServiceClient.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/FriendsServiceClient.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Convey.HTTP; using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Core.Wrappers; using MiniSpace.Services.Events.Application.Services.Clients; namespace MiniSpace.Services.Events.Infrastructure.Services.Clients @@ -20,7 +21,10 @@ public FriendsServiceClient(IHttpClient httpClient, HttpClientOptions options) _url = options.Services["friends"]; } - public Task> GetAsync(Guid studentId) - => _httpClient.GetAsync>($"{_url}/friends/{studentId}"); + public async Task> GetAsync(Guid studentId) + { + var pagedResponse = await _httpClient.GetAsync>($"{_url}/friends/{studentId}"); + return pagedResponse.Items; + } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/StudentsServiceClient.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/StudentsServiceClient.cs index 59334c418..c8896736b 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/StudentsServiceClient.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Clients/StudentsServiceClient.cs @@ -28,5 +28,8 @@ public async Task StudentExistsAsync(Guid id) return response != null; } + public Task GetStudentByIdAsync(Guid studentId) + => _httpClient.GetAsync($"{_url}/students/{studentId}"); + } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventRecommendationService.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventRecommendationService.cs new file mode 100644 index 000000000..d3a6a6995 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventRecommendationService.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.ML; +using Microsoft.ML.Data; +using MiniSpace.Services.Events.Application.DTO; +using MiniSpace.Services.Events.Application.Services; + +namespace MiniSpace.Services.Events.Infrastructure.Services.Recommendation +{ + public class EventRecommendationService : IEventRecommendationService + { + private readonly MLContext _mlContext; + private readonly ILogger _logger; + + public EventRecommendationService(ILogger logger) + { + _mlContext = new MLContext(); + _logger = logger; + } + + public IEnumerable RankEventsByUserInterest(Guid userId, IEnumerable events, IEnumerable userInterests) + { + _logger.LogInformation("Starting RankEventsByUserInterest method."); + var stopwatch = Stopwatch.StartNew(); + + // Convert user interests to HashSet for quick lookups + var userInterestsSet = new HashSet(userInterests, StringComparer.OrdinalIgnoreCase); + + // Prepare input data and filter out unlikely matches early to reduce the data size + var inputData = events + .Where(e => userInterestsSet.Any(interest => e.Description.Contains(interest, StringComparison.OrdinalIgnoreCase))) + .Select(e => CreateInputModel(e, userInterestsSet)) + .ToArray(); + + // Return early if no events match the user's interests + if (inputData.Length == 0) + { + _logger.LogInformation("No events matched user interests. Returning empty list."); + return Enumerable.Empty(); + } + + // Train model dynamically for the specific user + var userModel = TrainUserModel(inputData); + var predictionEngine = _mlContext.Model.CreatePredictionEngine(userModel); + + // Score events + var scoredEvents = inputData + .AsParallel() + .WithDegreeOfParallelism(Environment.ProcessorCount) + .Select(input => (Event: events.First(e => e.Id.ToString() == input.EventId), Score: predictionEngine.Predict(input).Score)) + .OrderByDescending(result => result.Score) + .Select(result => result.Event) + .ToList(); + + _logger.LogInformation("Completed RankEventsByUserInterest method in {TotalElapsedMilliseconds} ms.", stopwatch.ElapsedMilliseconds); + return scoredEvents; + } + + // Asynchronous version of the method + public async Task> RankEventsByUserInterestAsync(Guid userId, IEnumerable events, IEnumerable userInterests) + { + return await Task.Run(() => RankEventsByUserInterest(userId, events, userInterests)); + } + + private EventInputModel CreateInputModel(EventDto eventItem, HashSet userInterests) + { + var keywordMatches = userInterests.Count(interest => eventItem.Description.Contains(interest, StringComparison.OrdinalIgnoreCase)); + return new EventInputModel + { + EventId = eventItem.Id.ToString(), + TextLength = eventItem.Description?.Length ?? 0, + KeywordMatchCount = keywordMatches, + EventAgeDays = eventItem.StartDate != DateTime.MinValue ? (float)(DateTime.UtcNow - eventItem.StartDate).TotalDays : 0, + Label = keywordMatches // Use keyword matches as the label + }; + } + + private ITransformer TrainUserModel(EventInputModel[] inputData) + { + var trainingData = _mlContext.Data.LoadFromEnumerable(inputData); + + var dataProcessPipeline = _mlContext.Transforms.Concatenate("Features", nameof(EventInputModel.TextLength), + nameof(EventInputModel.KeywordMatchCount), + nameof(EventInputModel.EventAgeDays)); + + var trainer = _mlContext.Regression.Trainers.Sdca(labelColumnName: "Label", featureColumnName: "Features"); + var trainingPipeline = dataProcessPipeline.Append(trainer); + + // Train the model dynamically for the user + return trainingPipeline.Fit(trainingData); + } + } +} + diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventService.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventService.cs index 8c16deb4d..96a85cfab 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventService.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventService.cs @@ -9,9 +9,9 @@ using MiniSpace.Services.Events.Application.Exceptions; using MiniSpace.Services.Events.Application.Services; using MiniSpace.Services.Events.Application.Services.Clients; -using MiniSpace.Services.Events.Application.Wrappers; using MiniSpace.Services.Events.Core.Entities; using MiniSpace.Services.Events.Core.Repositories; +using MiniSpace.Services.Events.Core.Wrappers; namespace MiniSpace.Services.Events.Infrastructure.Services { @@ -32,7 +32,7 @@ public EventService(IEventRepository eventRepository, IEventValidator eventValid _appContext = appContext; } - public async Task>> BrowseEventsAsync(SearchEvents command) + public async Task> BrowseEventsAsync(SearchEvents command) { var dateFrom = DateTime.MinValue; var dateTo = DateTime.MinValue; @@ -40,24 +40,25 @@ public async Task>> BrowseEventsAsync(Search State? state = null; EventEngagementType? friendsEngagementType = null; IEnumerable organizations = new List(); - if(command.DateFrom != string.Empty) + + if (command.DateFrom != string.Empty) { - dateFrom =_eventValidator.ParseDate(command.DateFrom, "DateFrom"); + dateFrom = _eventValidator.ParseDate(command.DateFrom, "DateFrom"); } - if(command.DateTo != string.Empty) + if (command.DateTo != string.Empty) { dateTo = _eventValidator.ParseDate(command.DateTo, "DateTo"); } - if(command.Category != string.Empty) + if (command.Category != string.Empty) { category = _eventValidator.ParseCategory(command.Category); } - if(command.State != string.Empty) + if (command.State != string.Empty) { state = _eventValidator.ParseState(command.State); state = _eventValidator.RestrictState(state); } - if(command.FriendsEngagementType != string.Empty) + if (command.FriendsEngagementType != string.Empty) { friendsEngagementType = _eventValidator.ParseEngagementType(command.FriendsEngagementType); } @@ -66,51 +67,63 @@ public async Task>> BrowseEventsAsync(Search organizations = await _organizationsServiceClient .GetAllChildrenOrganizations(command.OrganizationId) ?? new List(); } + (int pageNumber, int pageSize) = _eventValidator.PageFilter(command.Pageable.Page, command.Pageable.Size); - + var result = await _eventRepository.BrowseEventsAsync( pageNumber, pageSize, command.Name, command.Organizer, dateFrom, dateTo, category, state, organizations, command.Friends, friendsEngagementType, command.Pageable.Sort.SortBy, command.Pageable.Sort.Direction); - + var identity = _appContext.Identity; - var pagedEvents = new PagedResponse>(result.events.Select(e => new EventDto(e, identity.Id)), - result.pageNumber, result.pageSize, result.totalPages, result.totalElements); + var pagedEvents = new PagedResponse( + result.events.Select(e => new EventDto(e, identity.Id)), + result.pageNumber, + result.pageSize, + result.totalElements + ); return pagedEvents; } - - public async Task>> BrowseOrganizerEventsAsync(SearchOrganizerEvents command) + + public async Task> BrowseOrganizerEventsAsync(SearchOrganizerEvents command) { var identity = _appContext.Identity; - if(identity.IsAuthenticated && identity.Id != command.OrganizerId && !identity.IsAdmin) + if (identity.IsAuthenticated && identity.Id != command.OrganizerId && !identity.IsAdmin) { throw new UnauthorizedOrganizerEventsAccessException(command.OrganizerId, identity.Id); } + var dateFrom = DateTime.MinValue; var dateTo = DateTime.MinValue; State? state = null; - if(command.DateFrom != string.Empty) + + if (command.DateFrom != string.Empty) { - dateFrom =_eventValidator.ParseDate(command.DateFrom, "DateFrom"); + dateFrom = _eventValidator.ParseDate(command.DateFrom, "DateFrom"); } - if(command.DateTo != string.Empty) + if (command.DateTo != string.Empty) { dateTo = _eventValidator.ParseDate(command.DateTo, "DateTo"); } - if(command.State != string.Empty) + if (command.State != string.Empty) { state = _eventValidator.ParseState(command.State); } + (int pageNumber, int pageSize) = _eventValidator.PageFilter(command.Pageable.Page, command.Pageable.Size); - + var result = await _eventRepository.BrowseOrganizerEventsAsync( - pageNumber, pageSize, command.Name, command.OrganizerId, dateFrom, dateTo, + pageNumber, pageSize, command.Name, command.OrganizerId, dateFrom, dateTo, command.Pageable.Sort.SortBy, command.Pageable.Sort.Direction, state); - - var pagedEvents = new PagedResponse>(result.events.Select(e => new EventDto(e, _appContext.Identity.Id)), - result.pageNumber, result.pageSize, result.totalPages, result.totalElements); + + var pagedEvents = new PagedResponse( + result.events.Select(e => new EventDto(e, _appContext.Identity.Id)), + result.pageNumber, + result.pageSize, + result.totalElements + ); return pagedEvents; } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Recommendation/EventInputModel.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Recommendation/EventInputModel.cs new file mode 100644 index 000000000..bd0713a6b --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Recommendation/EventInputModel.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.ML.Data; + +namespace MiniSpace.Services.Events.Infrastructure.Services.Recommendation +{ + public class EventInputModel + { + [LoadColumn(0)] + public string EventId { get; set; } // Convert Guid to string to ensure compatibility + + [LoadColumn(1)] + public float TextLength { get; set; } + + [LoadColumn(2)] + public float KeywordMatchCount { get; set; } + + [LoadColumn(3)] + public float EventAgeDays { get; set; } + + [LoadColumn(4)] + public float Label { get; set; } + } + +} diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Recommendation/EventPrediction.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Recommendation/EventPrediction.cs new file mode 100644 index 000000000..c8219c5cf --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/Recommendation/EventPrediction.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.ML.Data; + +namespace MiniSpace.Services.Events.Infrastructure.Services.Recommendation +{ + public class EventPrediction + { + [ColumnName("Score")] + public float Score { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Program.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Program.cs index d31b1d386..d700f1dfa 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Program.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Api/Program.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using MiniSpace.Services.Friends.Application; +using MiniSpace.Services.Friends.Core.Wrappers; using MiniSpace.Services.Friends.Application.Commands; using MiniSpace.Services.Friends.Application.Dto; // using MiniSpace.Services.Friends.Application.Events; @@ -35,26 +36,33 @@ public static async Task Main(string[] args) .UseInfrastructure() .UseDispatcherEndpoints(endpoints => endpoints .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) + .Post("friends/requests/{userId}/accept", afterDispatch: (cmd, ctx) => ctx.Response.Ok()) + .Post("friends/requests/{userId}/decline", afterDispatch: (cmd, ctx) => ctx.Response.Ok()) + .Post("friends/{userId}/invite", afterDispatch: (cmd, ctx) => ctx.Response.Created($"friends/{ctx.Request.RouteValues["userId"]}/invite")) + + .Get>("friends/{userId}") + .Get>("friends/requests/{userId}") + .Get>("friends/pending/all") + .Get>("friends/requests/sent/{userId}") + + .Get>("friends/{userId}/followers") + .Get>("friends/{userId}/following") + + .Put("friends/requests/{userId}/withdraw", afterDispatch: (cmd, ctx) => ctx.Response.Ok()) + + // .Get>("friends/{studentId}", // ctx => new GetFriends { StudentId = Guid.Parse(ctx.Request.RouteValues["studentId"].ToString()) }, // (query, ctx) => ctx.Response.WriteAsJsonAsync(query), // Correctly define delegate with parameters // afterDispatch: ctx => ctx.Response.Ok()) - .Get>("friends/{studentId}") - .Get>("friends/requests/{studentId}") - // .Get>("friends/pending") - .Get>("friends/pending/all") - .Get>("friends/requests/sent/{studentId}") + // .Get("friends/requests/sent", ctx => // { // var query = new GetSentFriendRequests { StudentId = ctx.User.GetUserId() }; // return ctx.QueryDispatcher.QueryAsync(query); // }, afterDispatch: ctx => ctx.Response.WriteAsJsonAsync(ctx.Result)) - - .Post("friends/requests/{studentId}/accept", afterDispatch: (cmd, ctx) => ctx.Response.Ok()) - .Post("friends/requests/{studentId}/decline", afterDispatch: (cmd, ctx) => ctx.Response.Ok()) - .Put("friends/requests/{studentId}/withdraw", afterDispatch: (cmd, ctx) => ctx.Response.Ok()) .Delete("friends/{requesterId}/{friendId}/remove") - .Post("friends/{studentId}/invite", afterDispatch: (cmd, ctx) => ctx.Response.Created($"friends/{ctx.Request.RouteValues["studentId"]}/invite")))) + )) .UseLogging() .UseLogging() .Build() diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/AddFriend.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/AddFriend.cs deleted file mode 100644 index dc0491a11..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/AddFriend.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Convey.CQRS.Commands; - -namespace MiniSpace.Services.Friends.Application.Commands -{ - public class AddFriend : ICommand - { - public Guid RequesterId { get; } - public Guid FriendId { get; } - - public AddFriend(Guid requesterId, Guid friendId) - { - RequesterId = requesterId; - FriendId = friendId; - } - } -} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/AddFriendHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/AddFriendHandler.cs deleted file mode 100644 index f12812c33..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/AddFriendHandler.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Threading.Tasks; -using Convey.CQRS.Commands; -using MiniSpace.Services.Friends.Application.Exceptions; -using MiniSpace.Services.Friends.Core.Entities; -using MiniSpace.Services.Friends.Core.Repositories; -using MiniSpace.Services.Friends.Application.Services; - -namespace MiniSpace.Services.Friends.Application.Commands.Handlers -{ - public class AddFriendHandler : ICommandHandler - { - private readonly IFriendRepository _friendRepository; - private readonly IAppContext _appContext; - private readonly IMessageBroker _messageBroker; - private readonly IEventMapper _eventMapper; - - public AddFriendHandler(IFriendRepository friendRepository, IAppContext appContext, - IMessageBroker messageBroker, IEventMapper eventMapper) - { - _friendRepository = friendRepository; - _appContext = appContext; - _messageBroker = messageBroker; - _eventMapper = eventMapper; - } - - public async Task HandleAsync(AddFriend command, CancellationToken cancellationToken = default) - { - if (!ValidateAccessOrFail(command.RequesterId)) - { - throw new UnauthorizedFriendActionException(command.RequesterId, command.FriendId); - } - - - var alreadyFriends = await _friendRepository.IsFriendAsync(command.RequesterId, command.FriendId); - if (alreadyFriends) - { - throw new AlreadyFriendsException(command.RequesterId, command.FriendId); - } - - var requester = await _friendRepository.GetFriendshipAsync(command.RequesterId, command.FriendId); - if (requester == null) - { - throw new FriendshipNotFoundException(command.RequesterId, command.FriendId); - } - await _friendRepository.UpdateFriendshipAsync(requester); - var events = _eventMapper.MapAll(requester.Events); - - await _messageBroker.PublishAsync(events); - } - - private bool ValidateAccessOrFail(Guid requesterId) - { - var identity = _appContext.Identity; - return identity.IsAuthenticated && identity.Id == requesterId; - } - } -} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/FriendAddedHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/FriendAddedHandler.cs deleted file mode 100644 index 4fedfe525..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/FriendAddedHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Convey.CQRS.Events; -using MiniSpace.Services.Friends.Application.Events; -using MiniSpace.Services.Friends.Application.Exceptions; -using MiniSpace.Services.Friends.Application.Services; -using MiniSpace.Services.Friends.Core.Repositories; - -namespace MiniSpace.Services.Friends.Application.Commands.Handlers -{ - public class FriendAddedHandler : IEventHandler - { - private readonly IFriendRepository _friendRepository; - private readonly IEventMapper _eventMapper; - private readonly IMessageBroker _messageBroker; - - public FriendAddedHandler(IFriendRepository friendRepository, IEventMapper eventMapper, IMessageBroker messageBroker) - { - _friendRepository = friendRepository; - _eventMapper = eventMapper; - _messageBroker = messageBroker; - } - - public async Task HandleAsync(FriendAdded @event, CancellationToken cancellationToken) - { - var friendship = await _friendRepository.GetFriendshipAsync(@event.RequesterId, @event.FriendId); - if (friendship is null) - { - throw new FriendshipNotFoundException(@event.RequesterId, @event.FriendId); - } - - friendship.MarkAsConfirmed(); - await _friendRepository.UpdateAsync(friendship); - - var events = _eventMapper.MapAll(friendship.Events); - await _messageBroker.PublishAsync(events.ToArray()); - } - } -} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/InviteFriendHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/InviteFriendHandler.cs index 6da33e78f..4b7a042d4 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/InviteFriendHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/InviteFriendHandler.cs @@ -12,20 +12,20 @@ namespace MiniSpace.Services.Friends.Application.Commands.Handlers public class InviteFriendHandler : ICommandHandler { private readonly IFriendRequestRepository _friendRequestRepository; - private readonly IStudentRequestsRepository _studentRequestsRepository; + private readonly IUserRequestsRepository _userRequestsRepository; private readonly IMessageBroker _messageBroker; private readonly IEventMapper _eventMapper; private readonly IAppContext _appContext; public InviteFriendHandler( IFriendRequestRepository friendRequestRepository, - IStudentRequestsRepository studentRequestsRepository, + IUserRequestsRepository userRequestsRepository, IMessageBroker messageBroker, IEventMapper eventMapper, IAppContext appContext) { _friendRequestRepository = friendRequestRepository; - _studentRequestsRepository = studentRequestsRepository; + _userRequestsRepository = userRequestsRepository; _messageBroker = messageBroker; _eventMapper = eventMapper; _appContext = appContext; @@ -54,36 +54,26 @@ public async Task HandleAsync(InviteFriend command, CancellationToken cancellati state: FriendState.Requested ); - await AddOrUpdateStudentRequest(command.InviterId, friendRequest, FriendState.Requested); - await AddOrUpdateStudentRequest(command.InviteeId, friendRequest, FriendState.Pending); + await AddOrUpdateUserRequest(command.InviterId, friendRequest, FriendState.Requested); + await AddOrUpdateUserRequest(command.InviteeId, friendRequest, FriendState.Pending); // Publish FriendInvited Event var friendInvitedEvent = new FriendInvited(command.InviterId, command.InviteeId); string friendInvitedJson = JsonSerializer.Serialize(friendInvitedEvent); await _messageBroker.PublishAsync(friendInvitedEvent); - - // Publish FriendRequestCreated Event - var friendRequestCreatedEvent = new FriendRequestCreated(command.InviterId, command.InviteeId); - string friendRequestCreatedJson = JsonSerializer.Serialize(friendRequestCreatedEvent); - await _messageBroker.PublishAsync(friendRequestCreatedEvent); - - // Publish FriendRequestSent Event - var friendRequestSentEvent = new FriendRequestSent(command.InviterId, command.InviteeId); - string friendRequestSentJson = JsonSerializer.Serialize(friendRequestSentEvent); - await _messageBroker.PublishAsync(friendRequestSentEvent); } - private async Task AddOrUpdateStudentRequest(Guid studentId, FriendRequest friendRequest, FriendState state) + private async Task AddOrUpdateUserRequest(Guid userId, FriendRequest friendRequest, FriendState state) { - var studentRequests = await _studentRequestsRepository.GetAsync(studentId); - if (studentRequests == null) + var userRequests = await _userRequestsRepository.GetAsync(userId); + if (userRequests == null) { - studentRequests = new StudentRequests(studentId); - await _studentRequestsRepository.AddAsync(studentRequests); + userRequests = new UserRequests(userId); + await _userRequestsRepository.AddAsync(userRequests); } - studentRequests.AddRequest(friendRequest.InviterId, friendRequest.InviteeId, friendRequest.RequestedAt, state); - await _studentRequestsRepository.UpdateAsync(studentRequests.StudentId, studentRequests.FriendRequests); + userRequests.AddRequest(friendRequest.InviterId, friendRequest.InviteeId, friendRequest.RequestedAt, state); + await _userRequestsRepository.UpdateAsync(userRequests.UserId, userRequests.FriendRequests); } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendAcceptHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendAcceptHandler.cs index 017142249..e2817a671 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendAcceptHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendAcceptHandler.cs @@ -10,58 +10,45 @@ namespace MiniSpace.Services.Friends.Application.Commands.Handlers { public class PendingFriendAcceptHandler : ICommandHandler { - private readonly IStudentRequestsRepository _studentRequestsRepository; - private readonly IStudentFriendsRepository _studentFriendsRepository; + private readonly IUserRequestsRepository _userRequestsRepository; + private readonly IUserFriendsRepository _userFriendsRepository; private readonly IMessageBroker _messageBroker; private readonly IEventMapper _eventMapper; public PendingFriendAcceptHandler( - // IFriendRequestRepository friendRequestRepository, - IStudentFriendsRepository studentFriendsRepository, - IStudentRequestsRepository studentRequestsRepository, - // IFriendRepository friendRepository, + IUserRequestsRepository userFriendsRepository, + IUserFriendsRepository userRequestsRepository, IMessageBroker messageBroker, IEventMapper eventMapper) { - // _friendRequestRepository = friendRequestRepository; - _studentFriendsRepository = studentFriendsRepository; - _studentRequestsRepository = studentRequestsRepository; - // _friendRepository = friendRepository; + _userRequestsRepository = userFriendsRepository; + _userFriendsRepository = userRequestsRepository; _messageBroker = messageBroker; _eventMapper = eventMapper; } public async Task HandleAsync(PendingFriendAccept command, CancellationToken cancellationToken = default) { - // Retrieve and validate the friend request between the inviter and invitee - var inviterRequests = await _studentRequestsRepository.GetAsync(command.RequesterId); - var inviteeRequests = await _studentRequestsRepository.GetAsync(command.FriendId); + var inviterRequests = await _userRequestsRepository.GetAsync(command.RequesterId); + var inviteeRequests = await _userRequestsRepository.GetAsync(command.FriendId); var friendRequest = FindFriendRequest(inviterRequests, inviteeRequests, command.RequesterId, command.FriendId); var friendRequestInvitee = FindFriendRequest(inviteeRequests, inviterRequests, command.RequesterId, command.FriendId); - // Update the friend request state to accepted + friendRequest.State = FriendState.Accepted; friendRequestInvitee.State = FriendState.Accepted; - // Save the updated FriendRequest states - await _studentRequestsRepository.UpdateAsync(command.RequesterId, inviterRequests.FriendRequests); - await _studentRequestsRepository.UpdateAsync(command.FriendId, inviteeRequests.FriendRequests); + await _userRequestsRepository.UpdateAsync(command.RequesterId, inviterRequests.FriendRequests); + await _userRequestsRepository.UpdateAsync(command.FriendId, inviteeRequests.FriendRequests); + + CreateAndAddFriends(command.RequesterId, command.FriendId, FriendState.Accepted); var pendingFriendAcceptedEvent = new PendingFriendAccepted(command.RequesterId, command.FriendId); await _messageBroker.PublishAsync(pendingFriendAcceptedEvent); - // Create Friend relationships in both directions - CreateAndAddFriends(command.RequesterId, command.FriendId, FriendState.Accepted); - - // Publish related events - // var events = _eventMapper.MapAll(new Core.Events.PendingFriendAccepted(command.RequesterId, command.FriendId)); - // await _messageBroker.PublishAsync(events); - - } - private FriendRequest FindFriendRequest(StudentRequests inviter, StudentRequests invitee, Guid inviterId, Guid inviteeId) + private FriendRequest FindFriendRequest(UserRequests inviter, UserRequests invitee, Guid inviterId, Guid inviteeId) { - // Find the FriendRequest in both inviter and invitee collections return inviter.FriendRequests.FirstOrDefault(fr => fr.InviterId == inviterId && fr.InviteeId == inviteeId) ?? invitee.FriendRequests.FirstOrDefault(fr => fr.InviterId == inviterId && fr.InviteeId == inviteeId) ?? throw new FriendRequestNotFoundException(inviterId, inviteeId); @@ -69,17 +56,14 @@ private FriendRequest FindFriendRequest(StudentRequests inviter, StudentRequests private async void CreateAndAddFriends(Guid inviterId, Guid inviteeId, FriendState state) { - // Retrieve or initialize the StudentFriends for both inviter and invitee - var inviterFriends = await _studentFriendsRepository.GetAsync(inviterId) ?? new StudentFriends(inviterId); - var inviteeFriends = await _studentFriendsRepository.GetAsync(inviteeId) ?? new StudentFriends(inviteeId); + var inviterFriends = await _userFriendsRepository.GetAsync(inviterId) ?? new UserFriends(inviterId); + var inviteeFriends = await _userFriendsRepository.GetAsync(inviteeId) ?? new UserFriends(inviteeId); - // Add new Friend instances with accepted state inviterFriends.AddFriend(new Friend(inviterId, inviteeId, DateTime.UtcNow, state)); inviteeFriends.AddFriend(new Friend(inviteeId, inviterId, DateTime.UtcNow, state)); - // Update the StudentFriends repositories - await _studentFriendsRepository.AddOrUpdateAsync(inviterFriends); - await _studentFriendsRepository.AddOrUpdateAsync(inviteeFriends); + await _userFriendsRepository.AddOrUpdateAsync(inviterFriends); + await _userFriendsRepository.AddOrUpdateAsync(inviteeFriends); } } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendDeclineHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendDeclineHandler.cs index 1be3e5263..4f1f685b3 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendDeclineHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/PendingFriendDeclineHandler.cs @@ -2,7 +2,10 @@ using MiniSpace.Services.Friends.Core.Repositories; using MiniSpace.Services.Friends.Application.Exceptions; using MiniSpace.Services.Friends.Application.Services; +using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Application.Events.External; using System; +using System.Threading; using System.Threading.Tasks; namespace MiniSpace.Services.Friends.Application.Commands.Handlers @@ -31,17 +34,17 @@ public async Task HandleAsync(PendingFriendDecline command, CancellationToken ca throw new FriendshipNotFoundException(command.RequesterId, command.FriendId); } - if (friendRequest.State != Core.Entities.FriendState.Requested) + if (friendRequest.State != FriendState.Requested) { - throw new InvalidOperationException("Friend request is not in the correct state to be declined."); + throw new InvalidFriendRequestStateException(command.RequesterId, command.FriendId, friendRequest.State.ToString()); } friendRequest.Decline(); - friendRequest.State = Core.Entities.FriendState.Declined; + friendRequest.State = FriendState.Declined; await _friendRequestRepository.UpdateAsync(friendRequest); - // var events = _eventMapper.MapAll(friendRequest.Events); - // await _messageBroker.PublishAsync(events.ToArray()); + var pendingFriendDeclinedEvent = new PendingFriendDeclined(command.RequesterId, command.FriendId); + await _messageBroker.PublishAsync(pendingFriendDeclinedEvent); } } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/RemoveFriendHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/RemoveFriendHandler.cs index 6978c1686..a59addd46 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/RemoveFriendHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/RemoveFriendHandler.cs @@ -5,25 +5,31 @@ using MiniSpace.Services.Friends.Application.Services; using MiniSpace.Services.Friends.Core.Repositories; +/* + This will require an update: + - RemoveFriendHandler.cs should be responsible for handling the RemoveFriend command with + that in mind that it will remove a friend from the user's friend list but should leave the + removed frined requests as avaising lite to accept (make the user subscriber). +*/ namespace MiniSpace.Services.Friends.Application.Commands.Handlers { public class RemoveFriendHandler : ICommandHandler { - private readonly IStudentFriendsRepository _studentFriendsRepository; - private readonly IStudentRequestsRepository _studentRequestsRepository; + private readonly IUserFriendsRepository _userFriendsRepository; + private readonly IUserRequestsRepository _userRequestsRepository; private readonly IMessageBroker _messageBroker; private readonly IEventMapper _eventMapper; private readonly IAppContext _appContext; public RemoveFriendHandler( - IStudentFriendsRepository studentFriendsRepository, - IStudentRequestsRepository studentRequestsRepository, + IUserFriendsRepository userFriendsRepository, + IUserRequestsRepository userRequestsRepository, IMessageBroker messageBroker, IEventMapper eventMapper, IAppContext appContext) { - _studentFriendsRepository = studentFriendsRepository; - _studentRequestsRepository = studentRequestsRepository; + _userFriendsRepository = userFriendsRepository; + _userRequestsRepository = userRequestsRepository; _messageBroker = messageBroker; _eventMapper = eventMapper; _appContext = appContext; @@ -34,26 +40,23 @@ public async Task HandleAsync(RemoveFriend command, CancellationToken cancellati var identity = _appContext.Identity; Console.WriteLine($"Handling RemoveFriend for RequesterId: {command.RequesterId} and FriendId: {command.FriendId}. Authenticated: {identity.IsAuthenticated}"); - var requesterFriends = await _studentFriendsRepository.GetAsync(command.RequesterId); - var friendFriends = await _studentFriendsRepository.GetAsync(command.FriendId); + var requesterFriends = await _userFriendsRepository.GetAsync(command.RequesterId); + var friendFriends = await _userRequestsRepository.GetAsync(command.FriendId); if (requesterFriends == null || friendFriends == null) { throw new FriendshipNotFoundException(command.RequesterId, command.FriendId); } - // Call specific methods to remove the friend connection - await _studentFriendsRepository.RemoveFriendAsync(command.RequesterId, command.FriendId); - await _studentFriendsRepository.RemoveFriendAsync(command.FriendId, command.RequesterId); + await _userFriendsRepository.RemoveFriendAsync(command.RequesterId, command.FriendId); + await _userFriendsRepository.RemoveFriendAsync(command.FriendId, command.RequesterId); - // Remove the corresponding friend requests - await _studentRequestsRepository.RemoveFriendRequestAsync(command.RequesterId, command.FriendId); - await _studentRequestsRepository.RemoveFriendRequestAsync(command.FriendId, command.RequesterId); + await _userRequestsRepository.RemoveFriendRequestAsync(command.RequesterId, command.FriendId); + await _userRequestsRepository.RemoveFriendRequestAsync(command.FriendId, command.RequesterId); - // Publish events indicating the removal of pending friend requests - var eventToPublish = new PendingFriendDeclined(command.RequesterId, command.FriendId); + var eventToPublish = new FriendRemoved(command.RequesterId, command.FriendId); await _messageBroker.PublishAsync(eventToPublish); - var reciprocalEventToPublish = new PendingFriendDeclined(command.FriendId, command.RequesterId); + var reciprocalEventToPublish = new FriendRemoved(command.FriendId, command.RequesterId); await _messageBroker.PublishAsync(reciprocalEventToPublish); } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/SentFriendRequestWithdrawHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/SentFriendRequestWithdrawHandler.cs index 3399942f5..e16fbcb98 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/SentFriendRequestWithdrawHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/Handlers/SentFriendRequestWithdrawHandler.cs @@ -15,20 +15,20 @@ namespace MiniSpace.Services.Friends.Application.Commands.Handlers public class SentFriendRequestWithdrawHandler : ICommandHandler { private readonly IFriendRequestRepository _friendRequestRepository; - private readonly IStudentRequestsRepository _studentRequestsRepository; + private readonly IUserRequestsRepository _userRequestsRepository; private readonly IMessageBroker _messageBroker; private readonly IAppContext _appContext; private readonly ILogger _logger; public SentFriendRequestWithdrawHandler( IFriendRequestRepository friendRequestRepository, - IStudentRequestsRepository studentRequestsRepository, + IUserRequestsRepository userRequestsRepository, IMessageBroker messageBroker, IAppContext appContext, ILogger logger) { _friendRequestRepository = friendRequestRepository; - _studentRequestsRepository = studentRequestsRepository; + _userRequestsRepository = userRequestsRepository; _messageBroker = messageBroker; _appContext = appContext; _logger = logger; @@ -38,11 +38,9 @@ public async Task HandleAsync(SentFriendRequestWithdraw command, CancellationTok { _logger.LogInformation("Handling SentFriendRequestWithdraw command: InviterId: {InviterId}, InviteeId: {InviteeId}", command.InviterId, command.InviteeId); - // Fetching request details for both inviter and invitee - var inviterRequests = await _studentRequestsRepository.GetAsync(command.InviterId); - var inviteeRequests = await _studentRequestsRepository.GetAsync(command.InviteeId); + var inviterRequests = await _userRequestsRepository.GetAsync(command.InviterId); + var inviteeRequests = await _userRequestsRepository.GetAsync(command.InviteeId); - // Checking existence of friend requests in both inviter's and invitee's lists var friendRequestForInviter = inviterRequests?.FriendRequests.FirstOrDefault(fr => fr.InviterId == command.InviterId && fr.InviteeId == command.InviteeId); var friendRequestForInvitee = inviteeRequests?.FriendRequests.FirstOrDefault(fr => fr.InviteeId == command.InviteeId && fr.InviterId == command.InviterId); @@ -52,42 +50,38 @@ public async Task HandleAsync(SentFriendRequestWithdraw command, CancellationTok throw new FriendRequestNotFoundException(command.InviterId, command.InviteeId); } - // Update the state to Cancelled for both inviter and invitee friendRequestForInviter.State = FriendState.Cancelled; friendRequestForInvitee.State = FriendState.Cancelled; _logger.LogInformation("Updating friend request state to Cancelled for request ID: {RequestId}", friendRequestForInviter.Id); - // Remove the friend request from both inviter's and invitee's lists await UpdateAndSaveRequests(inviterRequests, friendRequestForInviter); await UpdateAndSaveRequests(inviteeRequests, friendRequestForInvitee); - // Optionally delete the friend request if no longer needed await _friendRequestRepository.DeleteAsync(friendRequestForInviter.Id); - // Publish the event await _messageBroker.PublishAsync(new FriendRequestWithdrawn(friendRequestForInviter.InviterId, friendRequestForInvitee.InviteeId)); - _logger.LogInformation("Published FriendRequestWithdrawn event for InviterId: {InviterId} and InviteeId: {InviteeId}", friendRequestForInviter.InviterId, friendRequestForInvitee.InviteeId); + _logger.LogInformation("Published FriendRequestWithdrawn event for InviterId: {InviterId} and InviteeId: {InviteeId}", + friendRequestForInviter.InviterId, friendRequestForInvitee.InviteeId); } - private async Task UpdateAndSaveRequests(StudentRequests requests, FriendRequest friendRequest) + private async Task UpdateAndSaveRequests(UserRequests requests, FriendRequest friendRequest) { if (requests == null) { - _logger.LogWarning("Received null StudentRequests object for FriendRequest ID: {FriendRequestId}", friendRequest.Id); + _logger.LogWarning("Received null UserRequests object for FriendRequest ID: {FriendRequestId}", friendRequest.Id); return; } if (!requests.FriendRequests.Any(fr => fr.Id == friendRequest.Id)) { - _logger.LogWarning("FriendRequest ID: {FriendRequestId} not found in the requests of Student ID: {StudentId}", friendRequest.Id, requests.StudentId); + _logger.LogWarning("FriendRequest ID: {FriendRequestId} not found in the requests of User ID: {UserId}", friendRequest.Id, requests.UserId); return; } requests.RemoveRequest(friendRequest.Id); - // Save the updated list back to the repository - await _studentRequestsRepository.UpdateAsync(requests.StudentId, requests.FriendRequests.ToList()); - _logger.LogInformation("Updated and saved requests successfully for StudentId: {StudentId}, Total Requests: {Count}", requests.StudentId, requests.FriendRequests.Count()); + await _userRequestsRepository.UpdateAsync(requests.UserId, requests.FriendRequests.ToList()); + _logger.LogInformation("Updated and saved requests successfully for UserId: {UserId}, Total Requests: {Count}", requests.UserId, requests.FriendRequests.Count()); } } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/StudentCreated.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/StudentCreated.cs deleted file mode 100644 index 3313d86bc..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/StudentCreated.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Convey.CQRS.Commands; - -namespace MiniSpace.Services.Friends.Application.Events -{ - public class StudentCreated : ICommand - { - public Guid StudentId { get; } - public string Email { get; } - - public StudentCreated(Guid studentId, string email) - { - StudentId = studentId; - Email = email; - } - } -} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/StudentDeleted.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/StudentDeleted.cs deleted file mode 100644 index c0962d17f..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Commands/StudentDeleted.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Convey.CQRS.Commands; - -namespace MiniSpace.Services.Friends.Application.Events -{ - public class StudentDeleted : ICommand - { - public Guid StudentId { get; } - - public StudentDeleted(Guid studentId) - { - StudentId = studentId; - } - } -} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendDto.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendDto.cs index 59502bf48..fdbf5b978 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendDto.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendDto.cs @@ -5,12 +5,8 @@ namespace MiniSpace.Services.Friends.Application.Dto public class FriendDto { public Guid Id { get; set; } - public string Email { get; set; } - public string FirstName { get; set; } - public Guid StudentId { get; set; } + public Guid UserId { get; set; } public Guid FriendId { get; set; } - public string LastName { get; set; } - public string FullName => $"{FirstName} {LastName}"; public DateTime CreatedAt { get; set; } public FriendState State { get; set; } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendRequestDto.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendRequestDto.cs index 6af0e7b5f..f3adf6415 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendRequestDto.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/FriendRequestDto.cs @@ -9,7 +9,7 @@ public class FriendRequestDto public Guid InviteeId { get; set; } public DateTime RequestedAt { get; set; } public FriendState State { get; set; } - public Guid StudentId { get; set; } + public Guid UserId { get; set; } } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentDto.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/UserDto.cs similarity index 96% rename from MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentDto.cs rename to MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/UserDto.cs index 31b917273..402597018 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentDto.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/UserDto.cs @@ -3,7 +3,7 @@ namespace MiniSpace.Services.Friends.Application.Dto { - public class StudentDto + public class UserDto { public Guid Id { get; set; } public string Email { get; set; } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentFriendsDto.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/UserFriendsDto.cs similarity index 71% rename from MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentFriendsDto.cs rename to MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/UserFriendsDto.cs index f4f3b744e..3763cc476 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentFriendsDto.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/UserFriendsDto.cs @@ -3,9 +3,9 @@ namespace MiniSpace.Services.Friends.Application.Dto { - public class StudentFriendsDto + public class UserFriendsDto { - public Guid StudentId { get; set; } + public Guid UserId { get; set; } public List Friends { get; set; } = new List(); } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentRequestsDto.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/UserRequestsDto.cs similarity index 76% rename from MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentRequestsDto.cs rename to MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/UserRequestsDto.cs index 7418eb157..c7ab90762 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/StudentRequestsDto.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Dto/UserRequestsDto.cs @@ -3,10 +3,10 @@ namespace MiniSpace.Services.Friends.Application.Dto { - public class StudentRequestsDto + public class UserRequestsDto { public Guid Id { get; set; } - public Guid StudentId { get; set; } + public Guid UserId { get; set; } public List FriendRequests { get; set; } = new List(); } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendAdded.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendAdded.cs deleted file mode 100644 index d99a8603b..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendAdded.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Convey.CQRS.Events; -using Convey.MessageBrokers; - -namespace MiniSpace.Services.Friends.Application.Events.External -{ - [Message("notifications")] - public class FriendAdded : IEvent - { - public Guid RequesterId { get; } - public Guid FriendId { get; } - - public FriendAdded(Guid requesterId, Guid friendId) - { - RequesterId = requesterId; - FriendId = friendId; - } - } -} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendInvitedHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendInvitedHandler.cs deleted file mode 100644 index c3220cd56..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendInvitedHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Convey.CQRS.Events; -using MiniSpace.Services.Friends.Application.Exceptions; -using MiniSpace.Services.Friends.Application.Services; -using MiniSpace.Services.Friends.Core.Entities; -using MiniSpace.Services.Friends.Core.Repositories; - -namespace MiniSpace.Services.Friends.Application.Events.External.Handlers -{ - public class FriendInvitedHandler : IEventHandler - { - private readonly IFriendRepository _friendRepository; - private readonly IEventMapper _eventMapper; - private readonly IMessageBroker _messageBroker; - - public FriendInvitedHandler(IFriendRepository friendRepository, IEventMapper eventMapper, IMessageBroker messageBroker) - { - _friendRepository = friendRepository; - _eventMapper = eventMapper; - _messageBroker = messageBroker; - } - - public async Task HandleAsync(FriendInvited @event, CancellationToken cancellationToken) - { - var now = DateTime.UtcNow; - var request = new FriendRequest( - inviterId: @event.InviterId, - inviteeId: @event.InviteeId, - requestedAt: now, - state: FriendState.Requested - ); - - await _friendRepository.AddInvitationAsync(request); - - var events = _eventMapper.MapAll(request.Events); - await _messageBroker.PublishAsync(events.ToArray()); - } - - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendRequestCreatedHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendRequestCreatedHandler.cs deleted file mode 100644 index c486dadb6..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendRequestCreatedHandler.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Convey.CQRS.Events; -using MiniSpace.Services.Friends.Application.Exceptions; -using MiniSpace.Services.Friends.Application.Services; -using MiniSpace.Services.Friends.Core.Entities; -using MiniSpace.Services.Friends.Core.Repositories; - -namespace MiniSpace.Services.Friends.Application.Events.External.Handlers -{ - public class FriendRequestCreatedHandler : IEventHandler - { - private readonly IFriendRepository _friendRepository; - private readonly IEventMapper _eventMapper; - private readonly IMessageBroker _messageBroker; - - public FriendRequestCreatedHandler(IFriendRepository friendRepository, IEventMapper eventMapper, IMessageBroker messageBroker) - { - _friendRepository = friendRepository; - _eventMapper = eventMapper; - _messageBroker = messageBroker; - } - - public async Task HandleAsync(FriendRequestCreated @event, CancellationToken cancellationToken) - { - var now = DateTime.UtcNow; - var request = new FriendRequest( - inviterId: @event.RequesterId, - inviteeId: @event.FriendId, - requestedAt: now, - state: FriendState.Requested - ); - - await _friendRepository.AddRequestAsync(request); - - var events = _eventMapper.MapAll(request.Events); - await _messageBroker.PublishAsync(events.ToArray()); - } - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendRequestSentHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendRequestSentHandler.cs deleted file mode 100644 index cabfd3801..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/FriendRequestSentHandler.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Convey.CQRS.Events; -using MiniSpace.Services.Friends.Application.Events.External; -using MiniSpace.Services.Friends.Application.Exceptions; -using MiniSpace.Services.Friends.Application.Services; -using MiniSpace.Services.Friends.Core.Repositories; -using MiniSpace.Services.Friends.Core.Entities; - -namespace MiniSpace.Services.Friends.Application.Commands.Handlers -{ - public class FriendRequestSentHandler : IEventHandler - { - private readonly IFriendRepository _friendRepository; - private readonly IEventMapper _eventMapper; - private readonly IMessageBroker _messageBroker; - private readonly IAppContext _appContext; - - public FriendRequestSentHandler(IFriendRepository friendRepository, IEventMapper eventMapper, IMessageBroker messageBroker, IAppContext appContext) - { - _friendRepository = friendRepository; - _eventMapper = eventMapper; - _messageBroker = messageBroker; - _appContext = appContext; - } - - // public async Task HandleAsync(FriendRequestSent @event, CancellationToken cancellationToken) - // { - // var now = DateTime.UtcNow; - // var request = new FriendRequest( - // inviterId: @event.InviterId, - // inviteeId: @event.InviteeId, - // requestedAt: now, - // state: FriendState.Requested - // ); - // await _friendRepository.AddRequestAsync(request); - - // var events = _eventMapper.MapAll(request.Events); - // await _messageBroker.PublishAsync(events.ToArray()); - // // Console.WriteLine($"FriendInvited event published: InviterId={@event.InviterId}, InviteeId={@event.InviteeId}"); - // } - - public async Task HandleAsync(FriendRequestSent @event, CancellationToken cancellationToken) - { - try - { - var request = new FriendRequest( - inviterId: @event.InviterId, - inviteeId: @event.InviteeId, - requestedAt: DateTime.UtcNow, - state: FriendState.Requested - ); - - // await _friendRepository.AddRequestAsync(request); - var events = _eventMapper.MapAll(request.Events); - await _messageBroker.PublishAsync(events.ToArray()); - } - catch (Exception ex) - { - // Console.WriteLine($"An error occurred while handling FriendRequestSent: {ex.Message}"); - throw; - } - } - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendAcceptedHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendAcceptedHandler.cs deleted file mode 100644 index f54ed37ef..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendAcceptedHandler.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Convey.CQRS.Events; -using MiniSpace.Services.Friends.Application.Exceptions; -using MiniSpace.Services.Friends.Application.Services; -using MiniSpace.Services.Friends.Core.Repositories; - -namespace MiniSpace.Services.Friends.Application.Events.External.Handlers -{ - public class PendingFriendAcceptedHandler : IEventHandler - { - private readonly IFriendRepository _friendRepository; - private readonly IEventMapper _eventMapper; - private readonly IMessageBroker _messageBroker; - private readonly IAppContext _appContext; - - public PendingFriendAcceptedHandler(IFriendRepository friendRepository, IEventMapper eventMapper, IMessageBroker messageBroker, IAppContext appContext) - { - _friendRepository = friendRepository; - _eventMapper = eventMapper; - _messageBroker = messageBroker; - _appContext = appContext; - } - - public async Task HandleAsync(PendingFriendAccepted @event, CancellationToken cancellationToken) - { - var friendship = await _friendRepository.GetFriendshipAsync(@event.RequesterId, @event.FriendId); - if (friendship == null) - { - throw new FriendshipNotFoundException(@event.RequesterId, @event.FriendId); - } - - friendship.MarkAsConfirmed(); - await _friendRepository.UpdateFriendshipAsync(friendship); - - if (await _friendRepository.GetFriendshipAsync(@event.FriendId, @event.RequesterId) == null) - { - var reciprocalFriendship = new Core.Entities.Friend(@event.FriendId, @event.RequesterId, DateTime.UtcNow, Core.Entities.FriendState.Accepted); - await _friendRepository.AddAsync(reciprocalFriendship); - reciprocalFriendship.MarkAsConfirmed(); - await _friendRepository.UpdateFriendshipAsync(reciprocalFriendship); - } - - // Publish the confirmation event - var confirmationEvent = new FriendshipConfirmed(@event.RequesterId, @event.FriendId); - await _messageBroker.PublishAsync(confirmationEvent); - } - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendDeclinedHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendDeclinedHandler.cs deleted file mode 100644 index 9e9ef8c60..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/Handlers/PendingFriendDeclinedHandler.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Convey.CQRS.Events; -using MiniSpace.Services.Friends.Application.Exceptions; -using MiniSpace.Services.Friends.Application.Services; -using MiniSpace.Services.Friends.Core.Entities; -using MiniSpace.Services.Friends.Core.Repositories; -using System.Text.Json; -using System; - -namespace MiniSpace.Services.Friends.Application.Events.External.Handlers -{ - public class PendingFriendDeclinedHandler : IEventHandler - { - private readonly IFriendRequestRepository _friendRequestRepository; - private readonly IEventMapper _eventMapper; - private readonly IMessageBroker _messageBroker; - private readonly IAppContext _appContext; - - public PendingFriendDeclinedHandler( - IFriendRequestRepository friendRequestRepository, - IEventMapper eventMapper, - IMessageBroker messageBroker, - IAppContext appContext) - { - _friendRequestRepository = friendRequestRepository; - _eventMapper = eventMapper; - _messageBroker = messageBroker; - _appContext = appContext; - } - - public async Task HandleAsync(PendingFriendDeclined @event, CancellationToken cancellationToken) - { - Console.WriteLine($"Handling event: {JsonSerializer.Serialize(@event)}"); - - Console.WriteLine($"Searching for friend request between {@event.RequesterId} and {@event.FriendId}"); - var friendRequest = await _friendRequestRepository.FindByInviterAndInvitee(@event.RequesterId, @event.FriendId); - - if (friendRequest == null) - { - Console.WriteLine("No friend request found, throwing exception."); - throw new FriendshipNotFoundException(@event.RequesterId, @event.FriendId); - } - - if (friendRequest.State != FriendState.Declined) - { - Console.WriteLine("Friend request found but not declined, declining now."); - friendRequest.Decline(); - friendRequest.State = FriendState.Declined; - await _friendRequestRepository.UpdateAsync(friendRequest); - } - - Console.WriteLine("Publishing events related to the decline."); - // var events = _eventMapper.MapAll(friendRequest.Events); - // await _messageBroker.PublishAsync(events.ToArray()); - } - } -} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendAdded.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendAdded.cs deleted file mode 100644 index 2e36a5504..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendAdded.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Convey.CQRS.Events; - -namespace MiniSpace.Services.Friends.Application.Events -{ - public class FriendAdded : IEvent - { - public Guid RequesterId { get; } - public Guid FriendId { get; } - - public FriendAdded(Guid requesterId, Guid friendId) - { - RequesterId = requesterId; - FriendId = friendId; - } - } -} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendInvited.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendInvited.cs similarity index 100% rename from MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendInvited.cs rename to MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendInvited.cs diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestAccepted.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestAccepted.cs deleted file mode 100644 index 618700354..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestAccepted.cs +++ /dev/null @@ -1,18 +0,0 @@ -// TODO: REMOVE. - -// using Convey.CQRS.Events; - -// namespace MiniSpace.Services.Friends.Application.Events -// { -// public class FriendRequestAccepted : IEvent -// { -// public Guid RequesterId { get; } -// public Guid FriendId { get; } - -// public FriendRequestAccepted(Guid requesterId, Guid friendId) -// { -// RequesterId = requesterId; -// FriendId = friendId; -// } -// } -// } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendRequestCreated.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestCreated.cs similarity index 100% rename from MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendRequestCreated.cs rename to MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestCreated.cs diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestRejected.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestRejected.cs deleted file mode 100644 index 2f298649a..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestRejected.cs +++ /dev/null @@ -1,18 +0,0 @@ -// TODO: REMOVE. - -// using Convey.CQRS.Events; - -// namespace MiniSpace.Services.Friends.Application.Events -// { -// public class FriendRequestRejected : IEvent -// { -// public Guid RequesterId { get; } -// public Guid FriendId { get; } - -// public FriendRequestRejected(Guid requesterId, Guid friendId) -// { -// RequesterId = requesterId; -// FriendId = friendId; -// } -// } -// } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendRequestSent.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestSent.cs similarity index 100% rename from MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/FriendRequestSent.cs rename to MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendRequestSent.cs diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendshipConfirmed.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendshipConfirmed.cs deleted file mode 100644 index d585d850d..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/FriendshipConfirmed.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Convey.CQRS.Events; - -namespace MiniSpace.Services.Friends.Application.Events -{ - public class FriendshipConfirmed : IEvent - { - public Guid RequesterId { get; } - public Guid FriendId { get; } - - public FriendshipConfirmed(Guid requesterId, Guid friendId) - { - RequesterId = requesterId; - FriendId = friendId; - } - } -} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/PendingFriendAccepted.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/PendingFriendAccepted.cs similarity index 100% rename from MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/PendingFriendAccepted.cs rename to MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/PendingFriendAccepted.cs diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/PendingFriendDeclined.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/PendingFriendDeclined.cs similarity index 100% rename from MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/External/PendingFriendDeclined.cs rename to MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/PendingFriendDeclined.cs diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/SentFriendRequestWithdraw.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/SentFriendRequestWithdraw.cs deleted file mode 100644 index dfadb41f5..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Events/SentFriendRequestWithdraw.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Convey.CQRS.Commands; -using Convey.CQRS.Events; -using System; - -namespace MiniSpace.Services.Friends.Application.Events -{ - public class SentFriendRequestWithdrawHandler : IEvent - { - public Guid RequesterId { get; } - public Guid FriendId { get; } - - public SentFriendRequestWithdrawHandler(Guid requesterId, Guid friendId) - { - RequesterId = requesterId; - FriendId = friendId; - } - } -} - diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/InvalidFriendRequestStateException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/InvalidFriendRequestStateException.cs new file mode 100644 index 000000000..bb3447f6b --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/InvalidFriendRequestStateException.cs @@ -0,0 +1,18 @@ +namespace MiniSpace.Services.Friends.Application.Exceptions +{ + public class InvalidFriendRequestStateException : AppException + { + public override string Code { get; } = "invalid_friend_request_state"; + public Guid RequesterId { get; } + public Guid FriendId { get; } + public string CurrentState { get; } + + public InvalidFriendRequestStateException(Guid requesterId, Guid friendId, string currentState) + : base($"Friend request between requester ID {requesterId} and friend ID {friendId} is in an invalid state '{currentState}' for this operation.") + { + RequesterId = requesterId; + FriendId = friendId; + CurrentState = currentState; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/InvalidRoleException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/InvalidRoleException.cs index 7b14f309e..1625a4307 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/InvalidRoleException.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/InvalidRoleException.cs @@ -5,7 +5,7 @@ public class InvalidRoleException : AppException public override string Code { get; } = "invalid_role"; public InvalidRoleException(Guid userId, string role, string requiredRole) - : base($"Student account will not be created for the user with id: {userId} " + + : base($"User account will not be created for the user with id: {userId} " + $"due to the invalid role: {role} (required: {requiredRole}).") { } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/StudentAlreadyCreatedException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/StudentAlreadyCreatedException.cs deleted file mode 100644 index ef79f514b..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/StudentAlreadyCreatedException.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MiniSpace.Services.Friends.Application.Exceptions -{ - public class StudentAlreadyCreatedException : AppException - { - public override string Code { get; } = "student_already_created"; - public Guid StudentId { get; } - - public StudentAlreadyCreatedException(Guid studentId) - : base($"Student with id: {studentId} was already created.") - { - StudentId = studentId; - } - } -} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/UnauthorizedStudentAccessException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/UnauthorizedStudentAccessException.cs deleted file mode 100644 index 2bcbf7d72..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/UnauthorizedStudentAccessException.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace MiniSpace.Services.Friends.Application.Exceptions -{ - public class UnauthorizedStudentAccessException : AppException - { - public override string Code { get; } = "unauthorized_student_access"; - public Guid StudentId { get; } - public Guid UserId { get; } - - public UnauthorizedStudentAccessException(Guid studentId, Guid userId) - : base($"Unauthorized access to student with id: '{studentId}' by user with id: '{userId}'.") - { - StudentId = studentId; - UserId = userId; - } - } -} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/UnauthorizedUserAccessException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/UnauthorizedUserAccessException.cs new file mode 100644 index 000000000..001776cfc --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Exceptions/UnauthorizedUserAccessException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Friends.Application.Exceptions +{ + public class UnauthorizedUserAccessException : AppException + { + public override string Code { get; } = "unauthorized_user_access"; + public Guid UserId { get; } + public Guid AccessUserId { get; } + + public UnauthorizedUserAccessException(Guid userId, Guid accessUserId) + : base($"Unauthorized access to user with id: '{userId}' by access user with id: '{userId}'.") + { + UserId = userId; + AccessUserId = accessUserId; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/IIdentityContext.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/IIdentityContext.cs index a3535849b..7502b0ec4 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/IIdentityContext.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/IIdentityContext.cs @@ -9,7 +9,6 @@ public interface IIdentityContext bool IsAuthenticated { get; } bool IsAdmin { get; } bool IsBanned { get; } - bool IsOrganizer { get; } IDictionary Claims { get; } } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFollowers.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFollowers.cs new file mode 100644 index 000000000..3a6b8c730 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFollowers.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Core.Wrappers; + +namespace MiniSpace.Services.Friends.Application.Queries +{ + public class GetFollowers : IQuery> + { + public Guid UserId { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + + public GetFollowers(Guid userId, int page = 1, int pageSize = 10) + { + UserId = userId; + Page = page; + PageSize = pageSize; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFollowing.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFollowing.cs new file mode 100644 index 000000000..4a052a45d --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFollowing.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Core.Wrappers; + +namespace MiniSpace.Services.Friends.Application.Queries +{ + public class GetFollowing : IQuery> + { + public Guid UserId { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + + public GetFollowing(Guid userId, int page = 1, int pageSize = 10) + { + UserId = userId; + Page = page; + PageSize = pageSize; + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriend.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriend.cs index 4845571bf..dcc6d4f35 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriend.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriend.cs @@ -6,6 +6,6 @@ namespace MiniSpace.Services.Friends.Application.Queries { public class GetFriend : IQuery { - public Guid StudentId { get; set; } + public Guid UserId { get; set; } } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriendRequests.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriendRequests.cs index 546f8ffbd..dd9aa644e 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriendRequests.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriendRequests.cs @@ -1,18 +1,22 @@ using Convey.CQRS.Queries; using System.Text.Json.Serialization; using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Core.Wrappers; using System.Collections.Generic; namespace MiniSpace.Services.Friends.Application.Queries { - public class GetFriendRequests : IQuery>, IQuery + public class GetFriendRequests : IQuery> { - public Guid StudentId { get; set; } + public Guid UserId { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } - [JsonConstructor] - public GetFriendRequests([property: JsonPropertyName("studentId")] Guid studentId) + public GetFriendRequests(Guid userId, int page = 1, int pageSize = 10) { - StudentId = studentId; + UserId = userId; + Page = page; + PageSize = pageSize; } } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriends.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriends.cs index 7d315d294..e3c0ee2d3 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriends.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetFriends.cs @@ -1,11 +1,21 @@ using Convey.CQRS.Queries; using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Core.Wrappers; using System.Collections.Generic; namespace MiniSpace.Services.Friends.Application.Queries { - public class GetFriends : IQuery> + public class GetFriends : IQuery> { - public Guid StudentId { get; set; } - } + public Guid UserId { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + + public GetFriends(Guid userId, int page = 1, int pageSize = 10) + { + UserId = userId; + Page = page; + PageSize = pageSize; + } + } } \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetIncomingFriendRequests.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetIncomingFriendRequests.cs index e99a8a30d..ef72a4c6c 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetIncomingFriendRequests.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetIncomingFriendRequests.cs @@ -1,12 +1,22 @@ using Convey.CQRS.Queries; using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Core.Wrappers; using System; using System.Collections.Generic; namespace MiniSpace.Services.Friends.Application.Queries { - public class GetIncomingFriendRequests : IQuery> + public class GetIncomingFriendRequests : IQuery> { - public Guid StudentId { get; set; } + public Guid UserId { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + + public GetIncomingFriendRequests(Guid userId, int page = 1, int pageSize = 10) + { + UserId = userId; + Page = page; + PageSize = pageSize; + } } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetSentFriendRequests.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetSentFriendRequests.cs index 11cf088da..566e36651 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetSentFriendRequests.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Queries/GetSentFriendRequests.cs @@ -1,11 +1,21 @@ using Convey.CQRS.Queries; using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Core.Wrappers; using System; namespace MiniSpace.Services.Friends.Application.Queries { - public class GetSentFriendRequests : IQuery> + public class GetSentFriendRequests : IQuery> { - public Guid StudentId { get; set; } + public Guid UserId { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + + public GetSentFriendRequests(Guid userId, int page = 1, int pageSize = 10) + { + UserId = userId; + Page = page; + PageSize = pageSize; + } } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/Clients/IStudentsServiceClient.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/Clients/IStudentsServiceClient.cs index 0232b6dfc..e743773cd 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/Clients/IStudentsServiceClient.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/Clients/IStudentsServiceClient.cs @@ -8,7 +8,7 @@ namespace MiniSpace.Services.Friends.Application.Services.Clients { public interface IStudentsServiceClient { - Task GetAsync(Guid id); - public Task> GetAllAsync(); + Task GetAsync(Guid id); + public Task> GetAllAsync(); } } \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IEventMapper.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IEventMapper.cs index e4ebf0a50..bd82e6c73 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IEventMapper.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Application/Services/IEventMapper.cs @@ -8,7 +8,6 @@ public interface IEventMapper { IEvent Map(IDomainEvent @event); IEnumerable MapAll(IEnumerable events); - // IEnumerable MapAll(IDomainEvent @event); IEnumerable MapAll(PendingFriendAccepted pendingFriendAccept); } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Friend.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Friend.cs index f65bd27c2..0dfa24868 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Friend.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Friend.cs @@ -7,25 +7,25 @@ namespace MiniSpace.Services.Friends.Core.Entities public class Friend : AggregateRoot { public Guid FriendId { get; private set; } - public Guid StudentId { get; private set; } + public Guid UserId { get; private set; } public FriendState FriendState { get; private set; } public DateTime CreatedAt { get; private set; } - public Friend(Guid studentId, Guid friendId, DateTime createdAt, FriendState state) + public Friend(Guid userId, Guid friendId, DateTime createdAt, FriendState state) { Id = Guid.NewGuid(); - StudentId = studentId; + UserId = userId; FriendId = friendId; CreatedAt = createdAt; FriendState = state; } - public static Friend CreateNewFriendship(Guid studentId, Guid friendId) + public static Friend CreateNewFriendship(Guid userId, Guid friendId) { - return new Friend(studentId, friendId, DateTime.UtcNow, FriendState.Accepted); + return new Friend(userId, friendId, DateTime.UtcNow, FriendState.Accepted); } - public void InviteFriend(Student inviter, Student invitee) + public void InviteFriend(User inviter, User invitee) { if (FriendState != FriendState.Unknown) { @@ -36,7 +36,7 @@ public void InviteFriend(Student inviter, Student invitee) AddEvent(new FriendInvited(this, newFriend)); } - public void AcceptFriendship(Student friend) + public void AcceptFriendship(User friend) { if (FriendState != FriendState.Requested) { @@ -76,7 +76,7 @@ public void MarkAsDeclined() - public void RemoveFriend(Student requester, Student friend) + public void RemoveFriend(User requester, User friend) { if (FriendState != FriendState.Accepted) { diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Friendship.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Friendship.cs deleted file mode 100644 index f7a8d6cb7..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Friendship.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using MiniSpace.Services.Friends.Core.Events; -using MiniSpace.Services.Friends.Core.Exceptions; - -namespace MiniSpace.Services.Friends.Core.Entities -{ - public class Friendship : AggregateRoot - { - public Guid RequesterId { get; private set; } - public Guid FriendId { get; private set; } - public DateTime CreatedAt { get; private set; } - public FriendState State { get; private set; } - - public Friendship(Guid requesterId, Guid friendId) - { - Id = Guid.NewGuid(); - RequesterId = requesterId; - FriendId = friendId; - State = FriendState.Requested; - CreatedAt = DateTime.UtcNow; - } - - public void MarkAsConfirmed() - { - if (State != FriendState.Requested) - throw new InvalidFriendshipStateException(Id, State.ToString(), "Requested"); - State = FriendState.Confirmed; - AddEvent(new FriendshipConfirmed(Id)); - } - - public void DeclineFriendship() - { - if (State != FriendState.Requested) - throw new InvalidOperationException("Friendship can only be declined if it is in the requested state."); - - State = FriendState.Declined; - // Assuming 'Id' is the ID of the current object and 'FriendId' is the ID of the friend. - AddEvent(new FriendshipDeclined(RequesterId, FriendId)); - } - - - } - -} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Student.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Student.cs deleted file mode 100644 index 29af756f4..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/Student.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace MiniSpace.Services.Friends.Core.Entities -{ - public class Student - { - public Guid Id { get; private set; } - public string FullName { get; private set; } - public string Email { get; private set; } - public string FirstName { get; private set; } - public string LastName { get; private set; } - - public Student(Guid id, string fullName) - { - Id = id; - FullName = fullName; - } - } -} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/User.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/User.cs new file mode 100644 index 000000000..3656850d3 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/User.cs @@ -0,0 +1,12 @@ +namespace MiniSpace.Services.Friends.Core.Entities +{ + public class User + { + public Guid Id { get; private set; } + + public User(Guid id) + { + Id = id; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/StudentFriends.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/UserFriends.cs similarity index 57% rename from MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/StudentFriends.cs rename to MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/UserFriends.cs index ace983866..92e812d1b 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/StudentFriends.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/UserFriends.cs @@ -5,29 +5,23 @@ namespace MiniSpace.Services.Friends.Core.Entities { - public class StudentFriends : AggregateRoot + public class UserFriends : AggregateRoot { - public Guid StudentId { get; private set; } + public Guid UserId { get; private set; } private List _friends; public IEnumerable Friends => _friends.AsReadOnly(); - public StudentFriends(Guid studentId) + public UserFriends(Guid userId) { - Id = studentId; - StudentId = studentId; + Id = userId; + UserId = userId; _friends = new List(); } - - // public void AddFriend(Friend friend) - // { - // _friends.Add(friend); - // } - public void AddFriend(Friend friend) { if (_friends.Any(f => f.FriendId == friend.FriendId)) { - throw new InvalidOperationException("This friend is already added."); + throw new FriendAlreadyAddedException(); } _friends.Add(friend); } @@ -35,10 +29,7 @@ public void AddFriend(Friend friend) public void RemoveFriend(Guid friendId) { var friend = _friends.FirstOrDefault(f => f.FriendId == friendId); - // if (friend == null) - // { - // throw new InvalidOperationException("Friend not found."); - // } + _friends.Remove(friend); } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/StudentRequests.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/UserRequests.cs similarity index 84% rename from MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/StudentRequests.cs rename to MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/UserRequests.cs index 4bacf82b6..29c9dc999 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/StudentRequests.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Entities/UserRequests.cs @@ -4,25 +4,22 @@ namespace MiniSpace.Services.Friends.Core.Entities { - public class StudentRequests : AggregateRoot + public class UserRequests : AggregateRoot { - public Guid StudentId { get; private set; } + public Guid UserId { get; private set; } private List _friendRequests; public IEnumerable FriendRequests => _friendRequests.AsReadOnly(); - public StudentRequests(Guid studentId) + public UserRequests(Guid userId) { Id = Guid.NewGuid(); - StudentId = studentId; + UserId = userId; _friendRequests = new List(); } public void AddRequest(Guid inviterId, Guid inviteeId, DateTime requestedAt, FriendState state) { - // if (state != FriendState.Requested || state != FriendState.Pending) - // throw new ArgumentException("Initial state must be 'Requested' or 'Pending' when adding a new friend request."); - var friendRequest = new FriendRequest(inviterId, inviteeId, requestedAt, state); _friendRequests.Add(friendRequest); AddEvent(new FriendRequestCreated(friendRequest)); diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendAdded.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendAdded.cs index 5ce2e7349..396edcc08 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendAdded.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendAdded.cs @@ -4,10 +4,10 @@ namespace MiniSpace.Services.Friends.Core.Events { public class FriendAdded : IDomainEvent { - public Student Requester { get; private set; } - public Student Friend { get; private set; } + public User Requester { get; private set; } + public User Friend { get; private set; } - public FriendAdded(Student requester, Student friend) + public FriendAdded(User requester, User friend) { Requester = requester; Friend = friend; diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRemoved.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRemoved.cs index 55e170c7d..61f6864c2 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRemoved.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendRemoved.cs @@ -4,10 +4,10 @@ namespace MiniSpace.Services.Friends.Core.Events { public class FriendRemoved : IDomainEvent { - public Student Requester { get; private set; } - public Student Friend { get; private set; } + public User Requester { get; private set; } + public User Friend { get; private set; } - public FriendRemoved(Student requester, Student friend) + public FriendRemoved(User requester, User friend) { Requester = requester; Friend = friend; diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendshipDeclined.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendshipDeclined.cs index c7d11ab68..15182527c 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendshipDeclined.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Events/FriendshipDeclined.cs @@ -2,7 +2,6 @@ namespace MiniSpace.Services.Friends.Core.Events { public class FriendshipDeclined : IDomainEvent { - // Adding separate properties for the requester and the friend public Guid RequesterId { get; } public Guid FriendId { get; } public FriendshipDeclined(Guid requesterId, Guid friendId) diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendAlreadyAddedException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendAlreadyAddedException.cs new file mode 100644 index 000000000..46ba88a5b --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendAlreadyAddedException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.Friends.Core.Exceptions +{ + public class FriendAlreadyAddedException : DomainException + { + public override string Code { get; } = "friend_already_added"; + + public FriendAlreadyAddedException() : base("This friend is already added.") + { + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendshipStateException.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendshipStateException.cs index ce636398d..06d41da29 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendshipStateException.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Exceptions/FriendshipStateException.cs @@ -5,14 +5,14 @@ namespace MiniSpace.Services.Friends.Core.Exceptions public class FriendshipStateException : DomainException { public override string Code { get; } = "friendship_state_error"; - public Guid StudentId { get; } + public Guid UserId { get; } public FriendState AttemptedState { get; } public FriendState CurrentState { get; } - public FriendshipStateException(Guid studentId, FriendState attemptedState, FriendState currentState) - : base($"Attempt to change friendship state to {attemptedState} from {currentState} failed for student ID {studentId}.") + public FriendshipStateException(Guid userId, FriendState attemptedState, FriendState currentState) + : base($"Attempt to change friendship state to {attemptedState} from {currentState} failed for user ID {userId}.") { - StudentId = studentId; + UserId = userId; AttemptedState = attemptedState; CurrentState = currentState; } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IFriendRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IFriendRepository.cs index 993323a81..ee1152ca7 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IFriendRepository.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IFriendRepository.cs @@ -8,8 +8,8 @@ namespace MiniSpace.Services.Friends.Core.Repositories public interface IFriendRepository { Task AddFriendAsync(Guid requesterId, Guid friendId); - Task> GetFriendsAsync(Guid studentId); - Task IsFriendAsync(Guid studentId, Guid potentialFriendId); + Task> GetFriendsAsync(Guid userId); + Task IsFriendAsync(Guid userId, Guid potentialFriendId); Task RemoveFriendAsync(Guid requesterId, Guid friendId); Task AcceptFriendInvitationAsync(Guid requesterId, Guid friendId); Task DeclineFriendInvitationAsync(Guid requesterId, Guid friendId); diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentFriendsRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentFriendsRepository.cs deleted file mode 100644 index 9989e2e47..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentFriendsRepository.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using MiniSpace.Services.Friends.Core.Entities; - -namespace MiniSpace.Services.Friends.Core.Repositories -{ - public interface IStudentFriendsRepository - { - Task GetAsync(Guid studentId); - Task> GetAllAsync(); - Task AddAsync(StudentFriends studentFriends); - Task UpdateAsync(StudentFriends studentFriends); - Task DeleteAsync(Guid studentId); - Task ExistsAsync(Guid studentId); - Task> GetFriendsAsync(Guid studentId); - Task AddOrUpdateAsync(StudentFriends studentFriends); - Task RemoveFriendAsync(Guid studentId, Guid friendId); - } -} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentRequestsRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentRequestsRepository.cs deleted file mode 100644 index 5130e7d23..000000000 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentRequestsRepository.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using MiniSpace.Services.Friends.Core.Entities; - -namespace MiniSpace.Services.Friends.Core.Repositories -{ - public interface IStudentRequestsRepository - { - Task GetAsync(Guid studentId); - Task> GetAllAsync(); - Task AddAsync(StudentRequests studentRequests); - Task UpdateAsync(StudentRequests studentRequests); - Task UpdateAsync(Guid studentId, IEnumerable updatedFriendRequests); - Task DeleteAsync(Guid studentId); - Task RemoveFriendRequestAsync(Guid requesterId, Guid friendId); - } -} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IUserFriendsRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IUserFriendsRepository.cs new file mode 100644 index 000000000..198c28d54 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IUserFriendsRepository.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Core.Repositories +{ + public interface IUserFriendsRepository + { + Task GetAsync(Guid userId); + Task> GetAllAsync(); + Task AddAsync(UserFriends userFriends); + Task UpdateAsync(UserFriends userFriends); + Task DeleteAsync(Guid userId); + Task ExistsAsync(Guid userId); + Task> GetFriendsAsync(Guid userId); + Task AddOrUpdateAsync(UserFriends userFriends); + Task RemoveFriendAsync(Guid userId, Guid friendId); + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IUserRepository.cs similarity index 61% rename from MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentRepository.cs rename to MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IUserRepository.cs index 4e6c92d38..915405fba 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IStudentRepository.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IUserRepository.cs @@ -2,11 +2,11 @@ namespace MiniSpace.Services.Friends.Core.Repositories { - public interface IStudentRepository + public interface IUserRepository { - Task GetAsync(Guid id); + Task GetAsync(Guid id); Task ExistsAsync(Guid id); - Task AddAsync(Student student); + Task AddAsync(User user); Task DeleteAsync(Guid id); } } \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IUserRequestsRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IUserRequestsRepository.cs new file mode 100644 index 000000000..49a3f4776 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Repositories/IUserRequestsRepository.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Friends.Core.Entities; + +namespace MiniSpace.Services.Friends.Core.Repositories +{ + public interface IUserRequestsRepository + { + Task GetAsync(Guid userId); + Task> GetAllAsync(); + Task AddAsync(UserRequests userRequests); + Task UpdateAsync(UserRequests userRequests); + Task UpdateAsync(Guid userId, IEnumerable updatedFriendRequests); + Task DeleteAsync(Guid userId); + Task RemoveFriendRequestAsync(Guid requesterId, Guid friendId); + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Wrappers/PagedResponse.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Wrappers/PagedResponse.cs new file mode 100644 index 000000000..e91ff5f14 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Core/Wrappers/PagedResponse.cs @@ -0,0 +1,28 @@ +namespace MiniSpace.Services.Friends.Core.Wrappers +{ + public class PagedResponse + { + public IEnumerable Items { get; } + public int TotalPages { get; } + public int TotalItems { get; } + public int PageSize { get; } + public int Page { get; } + public bool First { get; } + public bool Last { get; } + public bool Empty { get; } + public int? NextPage => Page < TotalPages ? Page + 1 : (int?)null; + public int? PreviousPage => Page > 1 ? Page - 1 : (int?)null; + + public PagedResponse(IEnumerable items, int page, int pageSize, int totalItems) + { + Items = items; + PageSize = pageSize; + TotalItems = totalItems; + TotalPages = pageSize > 0 ? (int)Math.Ceiling((decimal)totalItems / pageSize) : 0; + Page = page; + First = page == 1; + Last = page == TotalPages; + Empty = !items.Any(); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/IdentityContext.cs index 6b2c5b72c..653605cee 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/IdentityContext.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Contexts/IdentityContext.cs @@ -11,7 +11,6 @@ internal class IdentityContext : IIdentityContext public bool IsAuthenticated { get; } public bool IsAdmin { get; } public bool IsBanned { get; } - public bool IsOrganizer { get; } public IDictionary Claims { get; } = new Dictionary(); internal IdentityContext() @@ -30,7 +29,6 @@ internal IdentityContext(string id, string role, bool isAuthenticated, IDictiona IsAuthenticated = isAuthenticated; IsAdmin = Role.Equals("admin", StringComparison.InvariantCultureIgnoreCase); IsBanned = Role.Equals("banned", StringComparison.InvariantCultureIgnoreCase); - IsOrganizer = Role.Equals("organizer", StringComparison.InvariantCultureIgnoreCase); Claims = claims ?? new Dictionary(); Name = Claims.TryGetValue("name", out var name) ? name : string.Empty; Email = Claims.TryGetValue("email", out var email) ? email : string.Empty; diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Exceptions/ExceptionToMessageMapper.cs index d125750d0..8a8aacdc0 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Exceptions/ExceptionToMessageMapper.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -15,7 +15,6 @@ public object Map(Exception exception, object message) AlreadyFriendsException ex => new FriendAddingFailed(ex.RequesterId, ex.FriendId, ex.Message, "already_friends"), FriendshipNotFoundException ex => message switch { - AddFriend _ => new FriendAddingFailed(ex.RequesterId, ex.FriendId, ex.Message, "friendship_not_found"), RemoveFriend _ => new FriendRemovalFailed(ex.RequesterId, ex.FriendId, ex.Message, "friendship_not_found"), _ => null }, diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Extensions.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Extensions.cs index 657ba3b5e..b8cf933e6 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Extensions.cs @@ -29,7 +29,6 @@ using MiniSpace.Services.Friends.Application; using MiniSpace.Services.Friends.Application.Commands; using MiniSpace.Services.Friends.Application.Events.External; -using MiniSpace.Services.Friends.Application.Events.External.Handlers; using MiniSpace.Services.Friends.Application.Services; using MiniSpace.Services.Friends.Core.Repositories; using MiniSpace.Services.Friends.Infrastructure.Contexts; @@ -42,6 +41,7 @@ using MiniSpace.Services.Friends.Application.Events; using MiniSpace.Services.Notifications.Infrastructure.Services.Clients; using MiniSpace.Services.Friends.Application.Services.Clients; +using Convey.Logging.CQRS; namespace MiniSpace.Services.Friends.Infrastructure { @@ -51,8 +51,9 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) { builder.Services.AddTransient(); builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddTransient(); @@ -76,11 +77,11 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) .AddRedis() .AddMetrics() .AddJaeger() - .AddHandlersLogging() + .AddEventHandlersLogging() .AddMongoRepository("friendRequests") .AddMongoRepository("friends") - .AddMongoRepository("student-friends") - .AddMongoRepository("student-requests") + .AddMongoRepository("user-friends") + .AddMongoRepository("user-requests") .AddWebApiSwaggerDocs() .AddCertificateAuthentication() .AddSecurity(); @@ -96,18 +97,12 @@ public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app .UseMetrics() .UseCertificateAuthentication() .UseRabbitMq() - .SubscribeCommand() .SubscribeCommand() .SubscribeCommand() .SubscribeCommand() .SubscribeCommand() .SubscribeCommand() - // .SubscribeEvent() .SubscribeEvent() - // .SubscribeEvent() - // .SubscribeEvent() - // .SubscribeEvent() - // .SubscribeEvent() .SubscribeEvent(); return app; diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/MessageToLogTemplateMapper.cs index b22cfb76f..166feb9b5 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/MessageToLogTemplateMapper.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -10,12 +10,6 @@ internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper private static IReadOnlyDictionary MessageTemplates => new Dictionary { - { - typeof(AddFriend), new HandlerLogTemplate - { - After = "Friendship added between requester: {RequesterId} and friend: {FriendId}." - } - }, { typeof(RemoveFriend), new HandlerLogTemplate { diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/Extensions.cs index 3595667fd..f9480d1d5 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/Extensions.cs @@ -7,13 +7,13 @@ namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Documents public static class Extensions { public static Friend AsEntity(this FriendDocument document) - => new Friend(document.StudentId, document.FriendId, document.CreatedAt, document.State); + => new Friend(document.UserId, document.FriendId, document.CreatedAt, document.State); public static FriendDocument AsDocument(this Friend entity) => new FriendDocument { Id = entity.Id, - StudentId = entity.StudentId, + UserId = entity.UserId, FriendId = entity.FriendId, CreatedAt = entity.CreatedAt, State = entity.FriendState @@ -23,13 +23,10 @@ public static FriendDto AsDto(this FriendDocument document) => new FriendDto { Id = document.Id, - StudentId = document.StudentId, + UserId = document.UserId, FriendId = document.FriendId, CreatedAt = document.CreatedAt, State = document.State, - // Email = document.Email, - // FirstName = document.FirstName, - // LastName = document.LastName }; public static FriendRequest AsEntity(this FriendRequestDocument document) @@ -65,48 +62,36 @@ public static FriendRequestDto AsDto(this FriendRequestDocument document) InviteeId = document.InviteeId, RequestedAt = document.RequestedAt, State = document.State, - StudentId = document.InviteeId + UserId = document.InviteeId }; - public static StudentFriendsDocument AsDocument(this StudentFriends entity) - => new StudentFriendsDocument + public static UserFriendsDocument AsDocument(this UserFriends entity) + => new UserFriendsDocument { Id = entity.Id, - StudentId = entity.StudentId, + UserId = entity.UserId, Friends = entity.Friends.Select(friend => friend.AsDocument()).ToList() }; - public static StudentFriends AsEntity(this StudentFriendsDocument document) - => new StudentFriends(document.StudentId); - - // With the correct definitions of the Object-Value method in Core. - // ... - // public static StudentFriends AsEntity(this StudentFriendsDocument document) - // { - // var studentFriends = new StudentFriends(document.StudentId); - // foreach (var friendDoc in document.Friends) - // { - // studentFriends.AddFriend(friendDoc.AsEntity()); - // } - // return studentFriends; - // } - - public static StudentRequestsDocument AsDocument(this StudentRequests entity) - => new StudentRequestsDocument + public static UserFriends AsEntity(this UserFriendsDocument document) + => new UserFriends(document.UserId); + + public static UserRequestsDocument AsDocument(this UserRequests entity) + => new UserRequestsDocument { Id = entity.Id, - StudentId = entity.StudentId, + UserId = entity.UserId, FriendRequests = entity.FriendRequests.Select(fr => fr.AsDocument()).ToList() }; - public static StudentRequests AsEntity(this StudentRequestsDocument document) + public static UserRequests AsEntity(this UserRequestsDocument document) { if (document == null) { throw new ArgumentNullException(nameof(document), "StudentRequestsDocument cannot be null."); } - var studentRequests = new StudentRequests(document.StudentId); + var studentRequests = new UserRequests(document.UserId); foreach (var friendRequestDoc in document.FriendRequests) { studentRequests.AddRequest(friendRequestDoc.InviterId, friendRequestDoc.InviteeId, friendRequestDoc.RequestedAt, friendRequestDoc.State); @@ -114,11 +99,11 @@ public static StudentRequests AsEntity(this StudentRequestsDocument document) return studentRequests; } - public static StudentRequestsDto AsDto(this StudentRequestsDocument document) - => new StudentRequestsDto + public static UserRequestsDto AsDto(this UserRequestsDocument document) + => new UserRequestsDto { Id = document.Id, - StudentId = document.StudentId, + UserId = document.UserId, FriendRequests = document.FriendRequests.Select(fr => fr.AsDto()).ToList() }; diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/FriendDocument.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/FriendDocument.cs index 4e1d6f6c5..a63b8ef00 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/FriendDocument.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/FriendDocument.cs @@ -7,12 +7,8 @@ public class FriendDocument : IIdentifiable { public Guid Id { get; set; } public Guid FriendId { get; set; } - public Guid StudentId { get; set; } + public Guid UserId { get; set; } public DateTime CreatedAt { get; set; } public FriendState State { get; set; } - - // public string Email { get; set; } - // public string FirstName { get; set; } - // public string LastName { get; set; } } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/StudentFriendsDocument.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/UserFriendsDocument.cs similarity index 65% rename from MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/StudentFriendsDocument.cs rename to MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/UserFriendsDocument.cs index f09c326ef..3572c1c95 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/StudentFriendsDocument.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/UserFriendsDocument.cs @@ -5,10 +5,10 @@ namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Documents { - public class StudentFriendsDocument : IIdentifiable + public class UserFriendsDocument : IIdentifiable { public Guid Id { get; set; } - public Guid StudentId { get; set; } - public List Friends { get; set; } = new List(); // List of friend documents + public Guid UserId { get; set; } + public List Friends { get; set; } = new List(); } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/StudentRequestsDocument.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/UserRequestsDocument.cs similarity index 76% rename from MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/StudentRequestsDocument.cs rename to MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/UserRequestsDocument.cs index ee250c2dd..0b30dd949 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/StudentRequestsDocument.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Documents/UserRequestsDocument.cs @@ -5,10 +5,10 @@ namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Documents { - public class StudentRequestsDocument : IIdentifiable + public class UserRequestsDocument : IIdentifiable { public Guid Id { get; set; } - public Guid StudentId { get; set; } + public Guid UserId { get; set; } public List FriendRequests { get; set; } = new List(); } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFollowersHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFollowersHandler.cs new file mode 100644 index 000000000..f156041d5 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFollowersHandler.cs @@ -0,0 +1,80 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Application.Queries; +using MiniSpace.Services.Friends.Core.Repositories; +using MiniSpace.Services.Friends.Core.Wrappers; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers +{ + public class GetFollowersHandler : IQueryHandler> + { + private readonly IUserFriendsRepository _userFriendsRepository; + private readonly IMongoRepository _userRequestsRepository; + + public GetFollowersHandler(IUserFriendsRepository userFriendsRepository, IMongoRepository userRequestsRepository) + { + _userFriendsRepository = userFriendsRepository; + _userRequestsRepository = userRequestsRepository; + } + + public async Task> HandleAsync(GetFollowers query, CancellationToken cancellationToken) + { + // Step 1: Get all users who have sent friend requests to the user (followers) + var incomingRequestsFilter = Builders.Filter.Eq(doc => doc.UserId, query.UserId); + var incomingRequests = await _userRequestsRepository.Collection + .Find(incomingRequestsFilter) + .ToListAsync(cancellationToken); + + var followersFromRequests = incomingRequests + .SelectMany(doc => doc.FriendRequests + .Where(request => request.InviteeId == query.UserId && request.State == Core.Entities.FriendState.Requested) + .Select(request => new FriendDto + { + Id = request.Id, + UserId = request.InviterId, + FriendId = request.InviteeId, + CreatedAt = request.RequestedAt, + State = request.State + })) + .ToList(); + + // Step 2: Get all friends of the user (friends are also followers) + var friends = await _userFriendsRepository.GetFriendsAsync(query.UserId); + + var followersFromFriends = friends.Select(f => new FriendDto + { + Id = f.Id, + UserId = f.FriendId, + FriendId = f.UserId, + CreatedAt = f.CreatedAt, + State = f.FriendState + }).ToList(); + + // Step 3: Combine followers from requests and friends + var allFollowers = followersFromRequests.Concat(followersFromFriends).Distinct().ToList(); + + // Step 4: Paginate the combined followers list + var totalItems = allFollowers.Count; + var paginatedFollowers = allFollowers + .Skip((query.Page - 1) * query.PageSize) + .Take(query.PageSize) + .ToList(); + + var response = new UserFriendsDto + { + UserId = query.UserId, + Friends = paginatedFollowers + }; + + return new PagedResponse(new List { response }, query.Page, query.PageSize, totalItems); + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFollowingHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFollowingHandler.cs new file mode 100644 index 000000000..cebd0533f --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFollowingHandler.cs @@ -0,0 +1,74 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Friends.Application.Dto; +using MiniSpace.Services.Friends.Application.Queries; +using MiniSpace.Services.Friends.Core.Repositories; +using MiniSpace.Services.Friends.Core.Wrappers; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers +{ + public class GetFollowingHandler : IQueryHandler> + { + private readonly IUserFriendsRepository _userFriendsRepository; + private readonly IMongoRepository _userRequestsRepository; + + public GetFollowingHandler(IUserFriendsRepository userFriendsRepository, IMongoRepository userRequestsRepository) + { + _userFriendsRepository = userFriendsRepository; + _userRequestsRepository = userRequestsRepository; + } + + public async Task> HandleAsync(GetFollowing query, CancellationToken cancellationToken) + { + var friends = await _userFriendsRepository.GetFriendsAsync(query.UserId); + + var sentRequestsFilter = Builders.Filter.Eq(doc => doc.UserId, query.UserId); + var sentRequests = await _userRequestsRepository.Collection + .Find(sentRequestsFilter) + .ToListAsync(cancellationToken); + + var following = friends.Select(f => new FriendDto + { + Id = f.Id, + UserId = f.UserId, + FriendId = f.FriendId, + CreatedAt = f.CreatedAt, + State = f.FriendState + }).ToList(); + + var sentRequestDtos = sentRequests.SelectMany(doc => doc.FriendRequests + .Where(request => request.InviterId == query.UserId && request.State == Core.Entities.FriendState.Requested) + .Select(request => new FriendDto + { + Id = request.Id, + UserId = request.InviterId, + FriendId = request.InviteeId, // The invitee is the one being followed + CreatedAt = request.RequestedAt, + State = request.State + })).ToList(); + + following.AddRange(sentRequestDtos); + + var totalItems = following.Count; + var paginatedFollowing = following + .Skip((query.Page - 1) * query.PageSize) + .Take(query.PageSize) + .ToList(); + + var response = new UserFriendsDto + { + UserId = query.UserId, + Friends = paginatedFollowing + }; + + return new PagedResponse(new List { response }, query.Page, query.PageSize, totalItems); + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendHandler.cs index 4f7bd2a45..4038f64f4 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendHandler.cs @@ -18,9 +18,9 @@ public GetFriendHandler(IMongoRepository friendRepository) public async Task HandleAsync(GetFriend query, CancellationToken cancellationToken) { - var document = await _friendRepository.GetAsync(p => p.Id == query.StudentId); + var document = await _friendRepository.GetAsync(p => p.Id == query.UserId); if (document == null) - throw new FriendshipNotFoundException(query.StudentId); + throw new FriendshipNotFoundException(query.UserId); return document.AsDto(); } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendRequestsHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendRequestsHandler.cs index 338f4a43d..31ae8fbbb 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendRequestsHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendRequestsHandler.cs @@ -3,38 +3,44 @@ using MiniSpace.Services.Friends.Application.Dto; using MiniSpace.Services.Friends.Application.Queries; using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Core.Wrappers; using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; -using System.Text.Json; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers { - public class GetFriendRequestsHandler : IQueryHandler> + public class GetFriendRequestsHandler : IQueryHandler> { private readonly IMongoRepository _friendRequestRepository; - public GetFriendRequestsHandler(IMongoRepository friendRequestRepository) { _friendRequestRepository = friendRequestRepository; } - public async Task> HandleAsync(GetFriendRequests query, CancellationToken cancellationToken) + public async Task> HandleAsync(GetFriendRequests query, CancellationToken cancellationToken) { - string queryJson = JsonSerializer.Serialize(query); - // Console.WriteLine($"Handling GetFriendRequests: {queryJson}"); - // Console.WriteLine($"Handling GetFriendRequests for UserId: {query.StudentId}"); + var filter = Builders.Filter.And( + Builders.Filter.Eq(fr => fr.InviteeId, query.UserId), + Builders.Filter.Eq(fr => fr.State, FriendState.Requested) + ); - var documents = await _friendRequestRepository.FindAsync(p => p.InviteeId == query.StudentId && p.State == FriendState.Requested); - // Console.WriteLine($"Found {documents.Count()} friend requests."); + var totalItems = (int)await _friendRequestRepository.Collection.CountDocumentsAsync(filter, cancellationToken: cancellationToken); - if (!documents.Any()) - { - // Console.WriteLine($"No friend requests found for UserId: {query.StudentId}."); - return Enumerable.Empty(); - } + var documents = await _friendRequestRepository.Collection + .Find(filter) + .Skip((query.Page - 1) * query.PageSize) + .Limit(query.PageSize) + .ToListAsync(cancellationToken); - return documents.Select(doc => doc.AsDto()); - } + var friendRequests = documents.Select(doc => doc.AsDto()); + return new PagedResponse(friendRequests, query.Page, query.PageSize, totalItems); + } } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendsHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendsHandler.cs index dfb1094f1..b6c94b422 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendsHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetFriendsHandler.cs @@ -2,48 +2,68 @@ using MiniSpace.Services.Friends.Application.Dto; using MiniSpace.Services.Friends.Application.Queries; using MiniSpace.Services.Friends.Core.Repositories; -using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Friends.Core.Wrappers; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers { - public class GetFriendsHandler : IQueryHandler> + public class GetFriendsHandler : IQueryHandler> { - private readonly IStudentFriendsRepository _studentFriendsRepository; + private readonly IUserFriendsRepository _userFriendsRepository; + private readonly ILogger _logger; - public GetFriendsHandler(IStudentFriendsRepository studentFriendsRepository) + public GetFriendsHandler(IUserFriendsRepository userFriendsRepository, ILogger logger) { - _studentFriendsRepository = studentFriendsRepository; + _userFriendsRepository = userFriendsRepository; + _logger = logger; } - public async Task> HandleAsync(GetFriends query, CancellationToken cancellationToken) + public async Task> HandleAsync(GetFriends query, CancellationToken cancellationToken) { - var friends = await _studentFriendsRepository.GetFriendsAsync(query.StudentId); - if (!friends.Any()) + _logger.LogInformation("Handling GetFriends query for UserId: {UserId}", query.UserId); + + var allFriends = await _userFriendsRepository.GetFriendsAsync(query.UserId); + + if (allFriends == null || !allFriends.Any()) { - return Enumerable.Empty(); + _logger.LogWarning("No friends found for UserId: {UserId}", query.UserId); + return new PagedResponse(Enumerable.Empty(), query.Page, query.PageSize, 0); } - return new List - { - new StudentFriendsDto + var totalItems = allFriends.Count(); + + var friendsToReturn = allFriends + .Skip((query.Page - 1) * query.PageSize) + .Take(query.PageSize) + .Select(f => new FriendDto { - StudentId = query.StudentId, - Friends = friends.Select(f => new FriendDto - { - Id = f.Id, - StudentId = f.StudentId, - FriendId = f.FriendId, - CreatedAt = f.CreatedAt, - State = f.FriendState - }).ToList() - } + Id = f.Id, + UserId = f.UserId, + FriendId = f.FriendId, + CreatedAt = f.CreatedAt, + State = f.FriendState + }) + .ToList(); + + var userFriendsDto = new UserFriendsDto + { + UserId = query.UserId, + Friends = friendsToReturn }; + + var userFriendsDtos = new List { userFriendsDto }; + + var jsonOptions = new JsonSerializerOptions { WriteIndented = true }; + var jsonString = JsonSerializer.Serialize(userFriendsDtos, jsonOptions); + _logger.LogInformation("Serialized UserFriendsDto JSON: {JsonString}", jsonString); + + return new PagedResponse(userFriendsDtos, query.Page, query.PageSize, totalItems); } } - } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetIncomingFriendRequestsHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetIncomingFriendRequestsHandler.cs index e58d0791b..7db86c872 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetIncomingFriendRequestsHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetIncomingFriendRequestsHandler.cs @@ -2,6 +2,7 @@ using Convey.Persistence.MongoDB; using MiniSpace.Services.Friends.Application.Dto; using MiniSpace.Services.Friends.Application.Queries; +using MiniSpace.Services.Friends.Core.Wrappers; using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; using MongoDB.Driver; using System; @@ -12,33 +13,38 @@ namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers { - public class GetIncomingFriendRequestsHandler : IQueryHandler> + public class GetIncomingFriendRequestsHandler : IQueryHandler> { - private readonly IMongoRepository _studentRequestsRepository; + private readonly IMongoRepository _userRequestsRepository; - public GetIncomingFriendRequestsHandler(IMongoRepository studentRequestsRepository) + public GetIncomingFriendRequestsHandler(IMongoRepository userRequestsRepository) { - _studentRequestsRepository = studentRequestsRepository; + _userRequestsRepository = userRequestsRepository; } - public async Task> HandleAsync(GetIncomingFriendRequests query, CancellationToken cancellationToken) + public async Task> HandleAsync(GetIncomingFriendRequests query, CancellationToken cancellationToken) { - var studentRequests = await _studentRequestsRepository.Collection - .Find(doc => doc.StudentId == query.StudentId) + var filter = Builders.Filter.Eq(doc => doc.UserId, query.UserId); + var totalItems = (int)await _userRequestsRepository.Collection.CountDocumentsAsync(filter, cancellationToken: cancellationToken); + + var userRequests = await _userRequestsRepository.Collection + .Find(filter) + .Skip((query.Page - 1) * query.PageSize) + .Limit(query.PageSize) .ToListAsync(cancellationToken); - if (studentRequests == null || !studentRequests.Any()) + if (userRequests == null || !userRequests.Any()) { - return Enumerable.Empty(); + return new PagedResponse(Enumerable.Empty(), query.Page, query.PageSize, 0); } - var incomingRequests = studentRequests - .Select(doc => new StudentRequestsDto + var incomingRequests = userRequests + .Select(doc => new UserRequestsDto { Id = doc.Id, - StudentId = doc.StudentId, + UserId = doc.UserId, FriendRequests = doc.FriendRequests - .Where(request => request.InviteeId == query.StudentId && request.State != Core.Entities.FriendState.Accepted) + .Where(request => request.InviteeId == query.UserId && request.State != Core.Entities.FriendState.Accepted) .Select(request => new FriendRequestDto { Id = request.Id, @@ -52,7 +58,7 @@ public async Task> HandleAsync(GetIncomingFriend .Where(dto => dto.FriendRequests.Any()) .ToList(); - return incomingRequests; + return new PagedResponse(incomingRequests, query.Page, query.PageSize, totalItems); } } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetSentFriendRequestsHandler.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetSentFriendRequestsHandler.cs index f144efc12..236f96111 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetSentFriendRequestsHandler.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Queries/Handlers/GetSentFriendRequestsHandler.cs @@ -2,6 +2,7 @@ using Convey.Persistence.MongoDB; using MiniSpace.Services.Friends.Application.Dto; using MiniSpace.Services.Friends.Application.Queries; +using MiniSpace.Services.Friends.Core.Wrappers; using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; using MongoDB.Driver; using System; @@ -12,48 +13,57 @@ namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Queries.Handlers { - public class GetSentFriendRequestsHandler : IQueryHandler> + public class GetSentFriendRequestsHandler : IQueryHandler> { - private readonly IMongoRepository _studentRequestsRepository; + private readonly IMongoRepository _userRequestsRepository; - public GetSentFriendRequestsHandler(IMongoRepository studentRequestsRepository) + public GetSentFriendRequestsHandler(IMongoRepository userRequestsRepository) { - _studentRequestsRepository = studentRequestsRepository; + _userRequestsRepository = userRequestsRepository; } - public async Task> HandleAsync(GetSentFriendRequests query, CancellationToken cancellationToken) + public async Task> HandleAsync(GetSentFriendRequests query, CancellationToken cancellationToken) { - var studentRequests = await _studentRequestsRepository.Collection - .Find(doc => doc.StudentId == query.StudentId) + // Fetch user requests from the database + var userRequests = await _userRequestsRepository.Collection + .Find(doc => doc.UserId == query.UserId) .ToListAsync(cancellationToken); - if (studentRequests == null || !studentRequests.Any()) + if (userRequests == null || !userRequests.Any()) { - return Enumerable.Empty(); + return new PagedResponse(Enumerable.Empty(), query.Page, query.PageSize, 0); } - var sentRequests = studentRequests - .Select(doc => new StudentRequestsDto + // Filter sent friend requests and map them to DTOs + var sentRequests = userRequests + .Select(doc => new UserRequestsDto { Id = doc.Id, - StudentId = doc.StudentId, + UserId = doc.UserId, FriendRequests = doc.FriendRequests - .Where(request => request.InviterId == query.StudentId && request.State == Core.Entities.FriendState.Requested) + .Where(request => request.InviterId == query.UserId && request.State == Core.Entities.FriendState.Requested) .Select(request => new FriendRequestDto { Id = request.Id, InviterId = request.InviterId, InviteeId = request.InviteeId, RequestedAt = request.RequestedAt, - State = request.State, - StudentId = request.InviterId + State = request.State }) .ToList() }) .Where(dto => dto.FriendRequests.Any()) .ToList(); - return sentRequests; + // Implement pagination + var totalItems = sentRequests.Count; + var paginatedRequests = sentRequests + .Skip((query.Page - 1) * query.PageSize) + .Take(query.PageSize) + .ToList(); + + // Return the paginated response + return new PagedResponse(paginatedRequests, query.Page, query.PageSize, totalItems); } } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendMongoRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendMongoRepository.cs index 4658140b4..f662ceab1 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendMongoRepository.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendMongoRepository.cs @@ -32,7 +32,7 @@ public Task AddFriendAsync(Guid requesterId, Guid friendId) var friend = new FriendDocument { Id = Guid.NewGuid(), - StudentId = requesterId, + UserId = requesterId, FriendId = friendId, CreatedAt = DateTime.UtcNow, State = FriendState.Requested @@ -58,25 +58,25 @@ public async Task ExistsAsync(Guid id) public async Task> GetFriendsAsync(Guid studentId) { - var documents = await _repository.FindAsync(f => f.StudentId == studentId); + var documents = await _repository.FindAsync(f => f.UserId == studentId); return documents?.Select(d => d.AsEntity()).ToList(); } public async Task GetFriendshipAsync(Guid requesterId, Guid friendId) { - var document = await _repository.GetAsync(f => f.StudentId == requesterId && f.FriendId == friendId); + var document = await _repository.GetAsync(f => f.UserId == requesterId && f.FriendId == friendId); return document?.AsEntity(); } public async Task IsFriendAsync(Guid studentId, Guid potentialFriendId) { - var friend = await _repository.GetAsync(f => f.StudentId == studentId && f.FriendId == potentialFriendId); + var friend = await _repository.GetAsync(f => f.UserId == studentId && f.FriendId == potentialFriendId); return friend != null; } public async Task RemoveFriendAsync(Guid requesterId, Guid friendId) { - var friend = await _repository.GetAsync(f => f.StudentId == requesterId && f.FriendId == friendId); + var friend = await _repository.GetAsync(f => f.UserId == requesterId && f.FriendId == friendId); if (friend != null) { await _repository.DeleteAsync(friend.Id); @@ -85,7 +85,7 @@ public async Task RemoveFriendAsync(Guid requesterId, Guid friendId) public async Task AcceptFriendInvitationAsync(Guid requesterId, Guid friendId) { - var friend = await _repository.GetAsync(f => f.StudentId == requesterId && f.FriendId == friendId); + var friend = await _repository.GetAsync(f => f.UserId == requesterId && f.FriendId == friendId); if (friend != null) { friend.State = FriendState.Accepted; @@ -95,7 +95,7 @@ public async Task AcceptFriendInvitationAsync(Guid requesterId, Guid friendId) public async Task DeclineFriendInvitationAsync(Guid requesterId, Guid friendId) { - var friend = await _repository.GetAsync(f => f.StudentId == requesterId && f.FriendId == friendId); + var friend = await _repository.GetAsync(f => f.UserId == requesterId && f.FriendId == friendId); if (friend != null) { friend.State = FriendState.Declined; @@ -107,7 +107,7 @@ public async Task InviteFriendAsync(Guid inviterId, Guid inviteeId) { var newFriend = new FriendDocument { - StudentId = inviterId, + UserId = inviterId, FriendId = inviteeId, CreatedAt = DateTime.UtcNow, State = FriendState.Requested @@ -137,7 +137,7 @@ public async Task AddAsync(Friend friend) var document = new FriendDocument { Id = friend.Id, - StudentId = friend.StudentId, + UserId = friend.UserId, FriendId = friend.FriendId, CreatedAt = friend.CreatedAt, State = friend.FriendState diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendRequestMongoRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendRequestMongoRepository.cs index d910bb3a4..55406e03e 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendRequestMongoRepository.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/FriendRequestMongoRepository.cs @@ -33,16 +33,11 @@ public async Task AddAsync(FriendRequest friendRequest) public async Task UpdateAsync(FriendRequest friendRequest) { - - var documentToUpdate = friendRequest.AsDocument(); + var documentToUpdate = friendRequest.AsDocument(); - documentToUpdate.State = friendRequest.State; + documentToUpdate.State = friendRequest.State; - // Console.WriteLine("Attempting to update document in database: " + JsonSerializer.Serialize(documentToUpdate)); - await _repository.UpdateAsync(documentToUpdate); - - var documentAfterUpdate = await _repository.GetAsync(friendRequest.Id); - // Console.WriteLine("Document after update: " + JsonSerializer.Serialize(documentAfterUpdate)); + await _repository.UpdateAsync(documentToUpdate); } public async Task DeleteAsync(Guid id) @@ -61,6 +56,5 @@ public async Task FindByInviterAndInvitee(Guid inviterId, Guid in var document = await _repository.FindAsync(fr => fr.InviterId == inviterId && fr.InviteeId == inviteeId); return document.FirstOrDefault()?.AsEntity(); } - } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/StudentFriendsMongoRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/StudentFriendsMongoRepository.cs index 872613e24..953e50e18 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/StudentFriendsMongoRepository.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/StudentFriendsMongoRepository.cs @@ -9,34 +9,34 @@ using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; using MongoDB.Driver; -public class StudentFriendsMongoRepository : IStudentFriendsRepository +public class UserFriendsMongoRepository : IUserFriendsRepository { - private readonly IMongoRepository _repository; + private readonly IMongoRepository _repository; - public StudentFriendsMongoRepository(IMongoRepository repository) + public UserFriendsMongoRepository(IMongoRepository repository) { _repository = repository; } - public async Task GetAsync(Guid studentId) + public async Task GetAsync(Guid studentId) { var document = await _repository.GetAsync(studentId); return document?.AsEntity(); } - public async Task> GetAllAsync() + public async Task> GetAllAsync() { var documents = await _repository.FindAsync(_ => true); return documents.Select(doc => doc.AsEntity()); } - public async Task AddAsync(StudentFriends studentFriends) + public async Task AddAsync(UserFriends studentFriends) { var document = studentFriends.AsDocument(); await _repository.AddAsync(document); } - public async Task UpdateAsync(StudentFriends studentFriends) + public async Task UpdateAsync(UserFriends studentFriends) { var document = studentFriends.AsDocument(); await _repository.UpdateAsync(document); @@ -55,79 +55,37 @@ public async Task ExistsAsync(Guid studentId) public async Task> GetFriendsAsync(Guid studentId) { - // Using a LINQ expression instead of a MongoDB filter - var documents = await _repository.FindAsync(doc => doc.StudentId == studentId); + var documents = await _repository.FindAsync(doc => doc.UserId == studentId); if (documents == null || !documents.Any()) { - // Console.WriteLine($"No document found for student ID: {studentId}"); return Enumerable.Empty(); } - var document = documents.First(); // Assuming you expect only one document per studentId or taking the first one - // Console.WriteLine($"Document found: {document.StudentId}, Friends Count: {document.Friends.Count}"); + var document = documents.First(); return document.Friends.Select(doc => new Friend( - doc.StudentId, + doc.UserId, doc.FriendId, doc.CreatedAt, doc.State)).ToList(); } - - - public async Task AddOrUpdateAsync(StudentFriends studentFriends) -{ - // Ensuring that the document ID (MongoDB _id) is explicitly set to StudentId - var filter = Builders.Filter.Eq(doc => doc.StudentId, studentFriends.StudentId); - var update = Builders.Update - .SetOnInsert(doc => doc.StudentId, studentFriends.StudentId) // Ensuring the document _id is set to StudentId on insert - .Set(doc => doc.Id, studentFriends.StudentId) // Setting the document _id field explicitly - .AddToSetEach(doc => doc.Friends, studentFriends.Friends.Select(f => f.AsDocument())); // Use AddToSetEach to append new items to the list - - var options = new UpdateOptions { IsUpsert = true }; - var result = await _repository.Collection.UpdateOneAsync(filter, update, options); - - // Console.WriteLine("********************************************************"); - // Check if the document was actually inserted or updated - if (result.ModifiedCount > 0 || result.UpsertedId != null) - { - // Retrieve the updated or inserted document - var updatedDocument = await _repository.GetAsync(studentFriends.StudentId); - if (updatedDocument != null) - { - // Serialize the updated document to JSON and log it - var json = JsonSerializer.Serialize(updatedDocument, new JsonSerializerOptions { WriteIndented = true }); - // Console.WriteLine("Updated StudentFriends document:"); - // Console.WriteLine(json); - } - else - { - // Console.WriteLine("Failed to retrieve the updated document."); - } - } - else + public async Task AddOrUpdateAsync(UserFriends studentFriends) { - // Console.WriteLine("No changes were made to the document."); + var filter = Builders.Filter.Eq(doc => doc.UserId, studentFriends.UserId); + var update = Builders.Update + .SetOnInsert(doc => doc.UserId, studentFriends.UserId) + .Set(doc => doc.Id, studentFriends.UserId) + .AddToSetEach(doc => doc.Friends, studentFriends.Friends.Select(f => f.AsDocument())); + + var options = new UpdateOptions { IsUpsert = true }; + await _repository.Collection.UpdateOneAsync(filter, update, options); } -} -public async Task RemoveFriendAsync(Guid studentId, Guid friendId) -{ - var filter = Builders.Filter.Eq(doc => doc.StudentId, studentId); - var update = Builders.Update.PullFilter(doc => doc.Friends, Builders.Filter.Eq("FriendId", friendId)); - - var result = await _repository.Collection.UpdateOneAsync(filter, update); - - if (result.ModifiedCount == 0) + public async Task RemoveFriendAsync(Guid studentId, Guid friendId) { - // Console.WriteLine($"No friend removed for Student ID: {studentId} with Friend ID: {friendId}"); - } - else - { - // Console.WriteLine($"Friend ID: {friendId} removed from Student ID: {studentId}'s friends list."); - } -} - - - + var filter = Builders.Filter.Eq(doc => doc.UserId, studentId); + var update = Builders.Update.PullFilter(doc => doc.Friends, Builders.Filter.Eq("FriendId", friendId)); + await _repository.Collection.UpdateOneAsync(filter, update); + } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/StudentRequestsMongoRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/StudentRequestsMongoRepository.cs index cf2e01c00..36394b1a7 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/StudentRequestsMongoRepository.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/StudentRequestsMongoRepository.cs @@ -11,19 +11,18 @@ namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Repositories { - public class StudentRequestsMongoRepository : IStudentRequestsRepository + public class UserRequestsMongoRepository : IUserRequestsRepository { - private readonly IMongoRepository _repository; + private readonly IMongoRepository _repository; - public StudentRequestsMongoRepository(IMongoRepository repository) + public UserRequestsMongoRepository(IMongoRepository repository) { _repository = repository; } - public async Task GetAsync(Guid studentId) + public async Task GetAsync(Guid studentId) { - // Console.WriteLine($"{studentId}"); - var document = await _repository.FindAsync(doc => doc.StudentId == studentId); + var document = await _repository.FindAsync(doc => doc.UserId == studentId); var studentRequestDocument = document.SingleOrDefault(); if (studentRequestDocument == null) { @@ -32,25 +31,24 @@ public async Task GetAsync(Guid studentId) var entity = studentRequestDocument.AsEntity(); var json = JsonSerializer.Serialize(entity, new JsonSerializerOptions { WriteIndented = true }); - // Console.WriteLine(json); return entity; } - public async Task> GetAllAsync() + public async Task> GetAllAsync() { var documents = await _repository.FindAsync(_ => true); return documents.Select(doc => doc.AsEntity()); } - public async Task AddAsync(StudentRequests studentRequests) + public async Task AddAsync(UserRequests studentRequests) { var document = studentRequests.AsDocument(); await _repository.AddAsync(document); } - public async Task UpdateAsync(StudentRequests studentRequests) + public async Task UpdateAsync(UserRequests studentRequests) { var document = studentRequests.AsDocument(); await _repository.UpdateAsync(document); @@ -58,45 +56,23 @@ public async Task UpdateAsync(StudentRequests studentRequests) public async Task UpdateAsync(Guid studentId, IEnumerable updatedFriendRequests) { - var document = await _repository.FindAsync(doc => doc.StudentId == studentId); + var document = await _repository.FindAsync(doc => doc.UserId == studentId); var studentRequestDocument = document.SingleOrDefault(); - // Console.WriteLine($"*******************************************************************************"); if (studentRequestDocument == null) { - // Console.WriteLine($"No document found with Student ID: {studentId}"); return; // Consider handling this case appropriately, possibly by adding a new document. } - - // Console.WriteLine($"Before update - Document JSON: {JsonSerializer.Serialize(studentRequestDocument, new JsonSerializerOptions { WriteIndented = true })}"); - - // Convert each FriendRequest to a FriendRequestDocument before assignment studentRequestDocument.FriendRequests = updatedFriendRequests.Select(fr => fr.AsDocument()).ToList(); - var filter = Builders.Filter.Eq(doc => doc.StudentId, studentRequestDocument.StudentId); - var update = Builders.Update.Set(doc => doc.FriendRequests, studentRequestDocument.FriendRequests); - - var result = await _repository.Collection.UpdateOneAsync(filter, update); - - // Fetch the updated document to log its new state - var updatedDocument = await _repository.FindAsync(doc => doc.StudentId == studentId); - var updatedStudentRequestDocument = updatedDocument.SingleOrDefault(); + var filter = Builders.Filter.Eq(doc => doc.UserId, studentRequestDocument.UserId); + var update = Builders.Update.Set(doc => doc.FriendRequests, studentRequestDocument.FriendRequests); - // Console.WriteLine($"After update - Document JSON: {JsonSerializer.Serialize(updatedStudentRequestDocument, new JsonSerializerOptions { WriteIndented = true })}"); - - if (result.ModifiedCount == 0) - { - // Console.WriteLine("No documents were modified during the update operation."); - throw new Exception("Update failed, no document was modified."); - } - else - { - // Console.WriteLine($"Document with Student ID: {studentId} was successfully updated. Modified count: {result.ModifiedCount}"); - } + await _repository.Collection.UpdateOneAsync(filter, update); } public async Task DeleteAsync(Guid studentId) { - var documents = await _repository.FindAsync(doc => doc.StudentId == studentId); + var documents = await _repository.FindAsync(doc => doc.UserId == studentId); var document = documents.SingleOrDefault(); if (document != null) { @@ -106,25 +82,19 @@ public async Task DeleteAsync(Guid studentId) public async Task RemoveFriendRequestAsync(Guid requesterId, Guid friendId) { - var filter = Builders.Filter.Eq(doc => doc.StudentId, requesterId) & - Builders.Filter.Or( - Builders.Filter.ElemMatch(doc => doc.FriendRequests, Builders.Filter.Eq(fr => fr.InviterId, friendId)), - Builders.Filter.ElemMatch(doc => doc.FriendRequests, Builders.Filter.Eq(fr => fr.InviteeId, friendId)) + var filter = Builders.Filter.Eq(doc => doc.UserId, requesterId) & + Builders.Filter.Or( + Builders.Filter.ElemMatch(doc => doc.FriendRequests, Builders.Filter.Eq(fr => fr.InviterId, friendId)), + Builders.Filter.ElemMatch(doc => doc.FriendRequests, Builders.Filter.Eq(fr => fr.InviteeId, friendId)) ); - var update = Builders.Update.PullFilter(doc => doc.FriendRequests, + var update = Builders.Update.PullFilter(doc => doc.FriendRequests, Builders.Filter.Or( Builders.Filter.Eq(fr => fr.InviterId, friendId), Builders.Filter.Eq(fr => fr.InviteeId, friendId) )); - var result = await _repository.Collection.UpdateOneAsync(filter, update); - - if (result.ModifiedCount == 0) - { - throw new Exception("No friend request was removed."); - } + await _repository.Collection.UpdateOneAsync(filter, update); } - } } diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/UserFriendsMongoRepository.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/UserFriendsMongoRepository.cs new file mode 100644 index 000000000..58037c4c5 --- /dev/null +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Mongo/Repositories/UserFriendsMongoRepository.cs @@ -0,0 +1,90 @@ +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Friends.Core.Entities; +using MiniSpace.Services.Friends.Core.Repositories; +using MiniSpace.Services.Friends.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Friends.Infrastructure.Mongo.Repositories +{ + public class UserFriendsMongoRepository : IUserFriendsRepository + { + private readonly IMongoRepository _repository; + + public UserFriendsMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid userId) + { + var document = await _repository.GetAsync(f => f.UserId == userId); + return document?.AsEntity(); + } + + public async Task> GetAllAsync() + { + var documents = await _repository.FindAsync(_ => true); + return documents.Select(d => d.AsEntity()); + } + + public async Task AddAsync(UserFriends userFriends) + { + var document = userFriends.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task UpdateAsync(UserFriends userFriends) + { + var document = userFriends.AsDocument(); + await _repository.UpdateAsync(document); + } + + public async Task DeleteAsync(Guid userId) + { + await _repository.DeleteAsync(userId); + } + + public async Task ExistsAsync(Guid userId) + { + var exists = await _repository.ExistsAsync(f => f.UserId == userId); + return exists; + } + + public async Task> GetFriendsAsync(Guid userId) + { + var document = await _repository.GetAsync(f => f.UserId == userId); + return document?.Friends.Select(f => f.AsEntity()) ?? Enumerable.Empty(); + } + + public async Task AddOrUpdateAsync(UserFriends userFriends) + { + var exists = await ExistsAsync(userFriends.UserId); + if (exists) + { + await UpdateAsync(userFriends); + } + else + { + await AddAsync(userFriends); + } + } + + public async Task RemoveFriendAsync(Guid userId, Guid friendId) + { + var document = await _repository.GetAsync(f => f.UserId == userId); + if (document != null) + { + var friend = document.Friends.FirstOrDefault(f => f.FriendId == friendId); + if (friend != null) + { + document.Friends.Remove(friend); + await _repository.UpdateAsync(document); + } + } + } + } +} diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/Clients/StudentsServiceClient.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/Clients/StudentsServiceClient.cs index 5e71d3cf1..e0432f70e 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/Clients/StudentsServiceClient.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/Clients/StudentsServiceClient.cs @@ -17,11 +17,11 @@ public StudentsServiceClient(IHttpClient httpClient, HttpClientOptions options) _url = options.Services["students"]; } - public Task GetAsync(Guid id) - => _httpClient.GetAsync($"{_url}/students/{id}"); + public Task GetAsync(Guid id) + => _httpClient.GetAsync($"{_url}/students/{id}"); - public Task> GetAllAsync() - => _httpClient.GetAsync>($"{_url}/students"); + public Task> GetAllAsync() + => _httpClient.GetAsync>($"{_url}/students"); } } \ No newline at end of file diff --git a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/EventMapper.cs index 6a5336143..53e480d66 100644 --- a/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/EventMapper.cs +++ b/MiniSpace.Services.Friends/src/MiniSpace.Services.Friends.Infrastructure/Services/EventMapper.cs @@ -16,9 +16,7 @@ public IEvent Map(IDomainEvent @event) { switch (@event) { - case Core.Events.FriendAdded e: - return new Application.Events.FriendAdded(e.Requester.Id, e.Friend.Id); - + case Core.Events.FriendRemoved e: return new Application.Events.FriendRemoved(e.Requester.Id, e.Friend.Id); diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs index 04323e506..4cc556507 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/Program.cs @@ -114,6 +114,11 @@ public static async Task Main(string[] args) var token = await ctx.RequestServices.GetService().VerifyTwoFactorCodeAsync(cmd); await ctx.Response.WriteJsonAsync(token); }) + .Put("users/status", async (cmd, ctx) => + { + await ctx.RequestServices.GetService().UpdateUserStatusAsync(cmd); + ctx.Response.StatusCode = 204; + }) )) .UseLogging() .Build() diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/SignIn.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/SignIn.cs index 5c4dbd598..1960c3c4e 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/SignIn.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/SignIn.cs @@ -7,11 +7,13 @@ public class SignIn : ICommand { public string Email { get; set; } public string Password { get; set; } + public string DeviceType { get; set; } - public SignIn(string email, string password) + public SignIn(string email, string password, string deviceType) { Email = email; Password = password; + DeviceType = deviceType; } } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/UpdateUserStatus.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/UpdateUserStatus.cs new file mode 100644 index 000000000..a935d7305 --- /dev/null +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Commands/UpdateUserStatus.cs @@ -0,0 +1,19 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Identity.Application.Commands +{ + public class UpdateUserStatus : ICommand + { + public Guid UserId { get; set; } + public bool IsOnline { get; set; } + public string DeviceType { get; set; } + + public UpdateUserStatus(Guid userId, bool isOnline, string deviceType) + { + UserId = userId; + IsOnline = isOnline; + DeviceType = deviceType; + } + } +} diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/AuthDto.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/AuthDto.cs index 736294d90..fad14dd4c 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/AuthDto.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/AuthDto.cs @@ -11,6 +11,8 @@ public class AuthDto public string Role { get; set; } public long Expires { get; set; } public bool IsTwoFactorRequired { get; set; } - public Guid UserId { get; set; } + public Guid UserId { get; set; } + public bool IsOnline { get; set; } + public string DeviceType { get; set; } } } diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs index d687cabf5..4708ac6fc 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/DTO/UserDto.cs @@ -19,9 +19,11 @@ public class UserDto public bool IsTwoFactorEnabled { get; set; } public string TwoFactorSecret { get; set; } - public UserDto() - { - } + public bool IsOnline { get; set; } + public string DeviceType { get; set; } + public DateTime? LastActive { get; set; } + + public UserDto() { } public UserDto(User user) { @@ -35,6 +37,10 @@ public UserDto(User user) EmailVerifiedAt = user.EmailVerifiedAt; IsTwoFactorEnabled = user.IsTwoFactorEnabled; TwoFactorSecret = user.TwoFactorSecret; + + IsOnline = user.IsOnline; + DeviceType = user.DeviceType; + LastActive = user.LastActive; } } } diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/IIdentityContext.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/IIdentityContext.cs index d5f13474d..16e10067b 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/IIdentityContext.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/IIdentityContext.cs @@ -12,7 +12,6 @@ public interface IIdentityContext bool IsAuthenticated { get; } bool IsAdmin { get; } bool IsBanned { get; } - bool IsOrganizer { get; } IDictionary Claims { get; } } } \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs index f92eebbf9..83b2d93a8 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/IIdentityService.cs @@ -20,5 +20,6 @@ public interface IIdentityService Task DisableTwoFactorAsync(DisableTwoFactor command); Task GenerateTwoFactorSecretAsync(GenerateTwoFactorSecret command); Task VerifyTwoFactorCodeAsync(VerifyTwoFactorCode command); + Task UpdateUserStatusAsync(UpdateUserStatus command); } } \ No newline at end of file diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/IdentityService.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/IdentityService.cs index 3a2b16074..2c68cb59f 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/IdentityService.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/IdentityService.cs @@ -94,16 +94,21 @@ public async Task SignInAsync(SignIn command) { claims.Add("permissions", user.Permissions); } + + user.SetOnlineStatus(true, command.DeviceType); + await _userRepository.UpdateAsync(user); + var auth = _jwtProvider.Create(user.Id, user.Role, claims: claims); auth.RefreshToken = await _refreshTokenService.CreateAsync(user.Id); + auth.IsOnline = true; + auth.DeviceType = command.DeviceType; + await _messageBroker.PublishAsync(new SignedIn(user.Id, user.Role)); return auth; } - - public async Task SignUpAsync(SignUp command) { if (!EmailRegex.IsMatch(command.Email)) @@ -318,5 +323,20 @@ public async Task VerifyTwoFactorCodeAsync(VerifyTwoFactorCode command) await _messageBroker.PublishAsync(new SignedIn(user.Id, user.Role)); return auth; } + + public async Task UpdateUserStatusAsync(UpdateUserStatus command) + { + var user = await _userRepository.GetAsync(command.UserId); + if (user == null) + { + throw new UserNotFoundException(command.UserId); + } + + user.SetOnlineStatus(command.IsOnline, command.DeviceType); + await _userRepository.UpdateAsync(user); + + _logger.LogInformation($"Updated status for user {command.UserId}: Online = {command.IsOnline}, DeviceType = {command.DeviceType}"); + } + } } diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/RefreshTokenService.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/RefreshTokenService.cs index 76a231e56..0af9993a6 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/RefreshTokenService.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Application/Services/Identity/RefreshTokenService.cs @@ -45,22 +45,37 @@ public async Task RevokeAsync(string refreshToken) token.Revoke(DateTime.UtcNow); await _refreshTokenRepository.UpdateAsync(token); + + var user = await _userRepository.GetAsync(token.UserId); + if (user != null) + { + user.SetOnlineStatus(false, null); + await _userRepository.UpdateAsync(user); + } } public async Task UseAsync(string refreshToken) { var token = await _refreshTokenRepository.GetAsync(refreshToken); - if (token is null) - { - throw new InvalidRefreshTokenException(); - } + User user = null; - if (token.Revoked) + if (token is null || token.Revoked) { - throw new RevokedRefreshTokenException(); + if (token?.UserId != null) + { + user = await _userRepository.GetAsync(token.UserId); + } + + if (user != null) + { + user.SetOnlineStatus(false, null); + await _userRepository.UpdateAsync(user); + } + + throw new InvalidRefreshTokenException(); } - var user = await _userRepository.GetAsync(token.UserId); + user = await _userRepository.GetAsync(token.UserId); if (user is null) { throw new UserNotFoundException(token.UserId); @@ -72,6 +87,7 @@ public async Task UseAsync(string refreshToken) ["permissions"] = user.Permissions } : null; + var auth = _jwtProvider.Create(token.UserId, user.Role, claims: claims); auth.RefreshToken = refreshToken; diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs index 09e8d1fb1..fc28cf090 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Entities/User.cs @@ -15,9 +15,12 @@ public class User : AggregateRoot public IEnumerable Permissions { get; private set; } public bool IsEmailVerified { get; set; } public string EmailVerificationToken { get; set; } - public DateTime? EmailVerifiedAt { get; set; } + public DateTime? EmailVerifiedAt { get; set; } public bool IsTwoFactorEnabled { get; set; } public string TwoFactorSecret { get; set; } + public bool IsOnline { get; private set; } + public string DeviceType { get; private set; } + public DateTime? LastActive { get; private set; } public User(Guid id, string name, string email, string password, Role role, DateTime createdAt, IEnumerable permissions = null) @@ -26,7 +29,7 @@ public User(Guid id, string name, string email, string password, Role role, Date { throw new InvalidNameException(name); } - + if (string.IsNullOrWhiteSpace(email)) { throw new InvalidEmailException(email); @@ -49,10 +52,14 @@ public User(Guid id, string name, string email, string password, Role role, Date Role = role; CreatedAt = createdAt; Permissions = permissions ?? Enumerable.Empty(); + + IsOnline = false; + DeviceType = null; + LastActive = DateTime.UtcNow; } internal User(Guid id, string name, string email, string password, Role role, DateTime createdAt, - bool isEmailVerified, string emailVerificationToken, DateTime? emailVerifiedAt, + bool isEmailVerified, string emailVerificationToken, DateTime? emailVerifiedAt, bool isTwoFactorEnabled, string twoFactorSecret, IEnumerable permissions = null) : this(id, name, email, password, role, createdAt, permissions) { @@ -62,7 +69,19 @@ internal User(Guid id, string name, string email, string password, Role role, Da IsTwoFactorEnabled = isTwoFactorEnabled; TwoFactorSecret = twoFactorSecret; } - + + public void SetOnlineStatus(bool isOnline, string deviceType) + { + IsOnline = isOnline; + DeviceType = isOnline ? deviceType : null; + LastActive = DateTime.UtcNow; + } + + public void UpdateLastActive() + { + LastActive = DateTime.UtcNow; + } + public void Ban() { if (Role == Role.Banned || Role == Role.Admin) @@ -72,7 +91,7 @@ public void Ban() Role = Role.Banned; } - + public void Unban() { if (Role != Role.Banned) @@ -137,6 +156,4 @@ public static class UserPermissions { public static string OrganizeEvents { get; private set; } = "organize_events"; } - - } diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBeBannedException.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBeBannedException.cs index 1aabc2fb9..7591334ac 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBeBannedException.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserCannotBeBannedException.cs @@ -8,7 +8,8 @@ public class UserCannotBeBannedException : DomainException public Guid UserId { get; } public string Role { get; } - public UserCannotBeBannedException(Guid userId, string role) : base($"User with ID: {userId} and Role: {role} cannot be banned.") + public UserCannotBeBannedException(Guid userId, string role) : + base($"User with ID: {userId} and Role: {role} cannot be banned.") { UserId = userId; Role = role; diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotBannedException.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotBannedException.cs index 34cfe793c..18491ded3 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotBannedException.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Core/Exceptions/UserIsNotBannedException.cs @@ -8,7 +8,8 @@ public class UserIsNotBannedException : DomainException public Guid UserId { get; } public string Role { get; } - public UserIsNotBannedException(Guid userId, string role) : base($"User with ID: {userId} and Role: {role} is not banned.") + public UserIsNotBannedException(Guid userId, string role) : + base($"User with ID: {userId} and Role: {role} is not banned.") { UserId = userId; Role = role; diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Contexts/IdentityContext.cs index e5b98897a..2738fe72f 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Contexts/IdentityContext.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Contexts/IdentityContext.cs @@ -15,7 +15,6 @@ internal class IdentityContext : IIdentityContext public bool IsAuthenticated { get; } public bool IsAdmin { get; } public bool IsBanned { get; } - public bool IsOrganizer { get; } public IDictionary Claims { get; } = new Dictionary(); internal IdentityContext() @@ -34,7 +33,6 @@ internal IdentityContext(string id, string role, bool isAuthenticated, IDictiona IsAuthenticated = isAuthenticated; IsAdmin = Role.Equals("admin", StringComparison.InvariantCultureIgnoreCase); IsBanned = Role.Equals("banned", StringComparison.InvariantCultureIgnoreCase); - IsOrganizer = Role.Equals("organizer", StringComparison.InvariantCultureIgnoreCase); Claims = claims ?? new Dictionary(); Name = Claims.TryGetValue("name", out var name) ? name : string.Empty; Email = Claims.TryGetValue("email", out var email) ? email : string.Empty; diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs index 8b6729bd1..d3b201985 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/Extensions.cs @@ -10,10 +10,9 @@ namespace MiniSpace.Services.Identity.Infrastructure.Mongo.Documents internal static class Extensions { public static User AsEntity(this UserDocument document) - => new User(document.Id, document.Name, document.Email, document.Password, - Enum.Parse(document.Role, true), document.CreatedAt, - document.Permissions - ) + { + var user = new User(document.Id, document.Name, document.Email, document.Password, + Enum.Parse(document.Role, true), document.CreatedAt, document.Permissions) { IsEmailVerified = document.IsEmailVerified, EmailVerificationToken = document.EmailVerificationToken, @@ -22,6 +21,12 @@ public static User AsEntity(this UserDocument document) TwoFactorSecret = document.TwoFactorSecret }; + user.SetOnlineStatus(document.IsOnline, document.DeviceType); + user.UpdateLastActive(); + + return user; + } + public static UserDocument AsDocument(this User entity) => new UserDocument { @@ -36,7 +41,10 @@ public static UserDocument AsDocument(this User entity) EmailVerificationToken = entity.EmailVerificationToken, EmailVerifiedAt = entity.EmailVerifiedAt, IsTwoFactorEnabled = entity.IsTwoFactorEnabled, - TwoFactorSecret = entity.TwoFactorSecret + TwoFactorSecret = entity.TwoFactorSecret, + IsOnline = entity.IsOnline, + DeviceType = entity.DeviceType, + LastActive = entity.LastActive }; public static UserDto AsDto(this UserDocument document) @@ -51,7 +59,10 @@ public static UserDto AsDto(this UserDocument document) IsEmailVerified = document.IsEmailVerified, EmailVerifiedAt = document.EmailVerifiedAt, IsTwoFactorEnabled = document.IsTwoFactorEnabled, - TwoFactorSecret = document.TwoFactorSecret + TwoFactorSecret = document.TwoFactorSecret, + IsOnline = document.IsOnline, + DeviceType = document.DeviceType, + LastActive = document.LastActive }; public static RefreshToken AsEntity(this RefreshTokenDocument document) diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/UserDocument.cs b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/UserDocument.cs index 2ecf2fc72..ebf458716 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/UserDocument.cs +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Infrastructure/Mongo/Documents/UserDocument.cs @@ -20,5 +20,9 @@ internal sealed class UserDocument : IIdentifiable public DateTime? EmailVerifiedAt { get; set; } public bool IsTwoFactorEnabled { get; set; } public string TwoFactorSecret { get; set; } + + public bool IsOnline { get; set; } + public string DeviceType { get; set; } + public DateTime? LastActive { get; set; } } } diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Program.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Program.cs index 56c7d8e45..914e85b0e 100644 --- a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Program.cs +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Program.cs @@ -48,7 +48,6 @@ await WebHost.CreateDefaultBuilder(args) { var fileId = await ctx.RequestServices.GetService().UploadFileAsync(cmd); await ctx.Response.WriteJsonAsync(fileId); - /// the probblems is that is is like writing the file id ? to responce only }) ) .UseDispatcherEndpoints(endpoints => endpoints diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs index 7f767584f..2020f659e 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Api/Program.cs @@ -38,12 +38,14 @@ public static async Task Main(string[] args) .Get>("organizations/{organizationId}/children") .Get>("organizations/{organizationId}/children/all") // the organizations users is the organizer - .Get>("users/{userId}/organizations") + .Get>("organizations/users/{userId}/organizations") // organizations, user is a part of - .Get>("users/{userId}/organizations/follow") + .Get>("organizations/users/{userId}/organizations/follow") .Get("organizations/{organizationId}/details/gallery-users") .Get>("organizations/{organizationId}/roles") .Get>("organizations/paginated") + .Get>("organizations/{organizationId}/requests") + .Post("organizations", afterDispatch: (cmd, ctx) => ctx.Response.Created($"organizations/{cmd.OrganizationId}")) diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/FollowOrganizationHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/FollowOrganizationHandler.cs index 2ae4a28ad..5eeed3c65 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/FollowOrganizationHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Commands/Handlers/FollowOrganizationHandler.cs @@ -75,7 +75,6 @@ public async Task HandleAsync(FollowOrganization command, CancellationToken canc var newUser = new User(command.UserId, defaultRole); await _organizationMembersRepository.AddMemberAsync(command.OrganizationId, newUser); - // Add the organization to the user's list of organizations await _userOrganizationsRepository.AddOrganizationToUserAsync(command.UserId, command.OrganizationId); // Publish event diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs index a5e0b36ed..d4dbff79e 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationDto.cs @@ -23,6 +23,7 @@ public class OrganizationDto public string Email { get; set; } public IEnumerable Users { get; set; } + public OrganizationSettingsDto Settings { get; set; } public OrganizationDto() { @@ -47,6 +48,7 @@ public OrganizationDto(Organization organization) // Convert each User entity to a UserDto Users = organization.Users?.Select(user => new UserDto(user)).ToList(); + Settings = organization.Settings != null ? new OrganizationSettingsDto(organization.Settings) : null; } } } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationRequestDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationRequestDto.cs new file mode 100644 index 000000000..f02c36f01 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationRequestDto.cs @@ -0,0 +1,27 @@ +using System; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Application.DTO +{ + public class OrganizationRequestDto + { + public Guid RequestId { get; set; } + public Guid UserId { get; set; } + public DateTime RequestDate { get; set; } + public string State { get; set; } + public string Reason { get; set; } + + // Factory method to create a DTO from an OrganizationRequest entity + public static OrganizationRequestDto FromEntity(OrganizationRequest request) + { + return new OrganizationRequestDto + { + RequestId = request.Id, + UserId = request.UserId, + RequestDate = request.RequestDate, + State = request.State.ToString(), + Reason = request.Reason + }; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationRequestsDto.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationRequestsDto.cs new file mode 100644 index 000000000..58180e0cc --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Dto/OrganizationRequestsDto.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Application.DTO +{ + public class OrganizationRequestsDto + { + public Guid OrganizationId { get; set; } + public IEnumerable Requests { get; set; } + + public OrganizationRequestsDto() + { + Requests = new List(); + } + + public static OrganizationRequestsDto FromEntity(Guid organizationId, IEnumerable requests) + { + var requestDtos = new List(); + foreach (var request in requests) + { + requestDtos.Add(OrganizationRequestDto.FromEntity(request)); + } + + return new OrganizationRequestsDto + { + OrganizationId = organizationId, + Requests = requestDtos + }; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetPaginatedOrganizationRequests.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetPaginatedOrganizationRequests.cs new file mode 100644 index 000000000..053e4eee9 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetPaginatedOrganizationRequests.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + public class GetPaginatedOrganizationRequests : IQuery> + { + public Guid OrganizationId { get; } + public int Page { get; } + public int PageSize { get; } + + public GetPaginatedOrganizationRequests(Guid organizationId, int page, int pageSize) + { + OrganizationId = organizationId; + Page = page; + PageSize = pageSize; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetPaginatedUserOrganizations.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetPaginatedUserOrganizations.cs new file mode 100644 index 000000000..75bbb18ce --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetPaginatedUserOrganizations.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; +using System; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + public class GetPaginatedUserOrganizations : IQuery> + { + public Guid UserId { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + + public GetPaginatedUserOrganizations(Guid userId, int page, int pageSize) + { + UserId = userId; + Page = page; + PageSize = pageSize; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetUserCreatedOrganizations.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetUserCreatedOrganizations.cs new file mode 100644 index 000000000..a9cba871f --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Application/Queries/GetUserCreatedOrganizations.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; +using System; + +namespace MiniSpace.Services.Organizations.Application.Queries +{ + public class GetUserCreatedOrganizations : IQuery> + { + public Guid UserId { get; } + public int Page { get; } + public int PageSize { get; } + + public GetUserCreatedOrganizations(Guid userId, int page, int pageSize) + { + UserId = userId; + Page = page; + PageSize = pageSize; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/OrganizationView.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/OrganizationView.cs new file mode 100644 index 000000000..410d92b1e --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/OrganizationView.cs @@ -0,0 +1,22 @@ +using System; + +namespace MiniSpace.Services.Organizations.Core.Entities +{ + public class OrganizationView + { + public Guid UserId { get; private set; } + public DateTime Date { get; private set; } + public string IpAddress { get; private set; } + public string DeviceType { get; private set; } + public string OperatingSystem { get; private set; } + + public OrganizationView(Guid userId, DateTime date, string ipAddress, string deviceType, string operatingSystem) + { + UserId = userId; + Date = date; + IpAddress = ipAddress; + DeviceType = deviceType; + OperatingSystem = operatingSystem; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/OrganizationViews.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/OrganizationViews.cs new file mode 100644 index 000000000..0f1e744a4 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/OrganizationViews.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Organizations.Core.Entities +{ + public class OrganizationViews + { + public Guid OrganizationId { get; private set; } + public IEnumerable Views { get; private set; } + + public OrganizationViews(Guid organizationId, IEnumerable views) + { + OrganizationId = organizationId; + Views = views ?? new List(); + } + + public void AddView(Guid userId, DateTime date, string ipAddress, string deviceType, string operatingSystem) + { + var viewList = new List(Views) + { + new OrganizationView(userId, date, ipAddress, deviceType, operatingSystem) + }; + Views = viewList; + } + + public void RemoveView(Guid userId) + { + var viewList = new List(Views); + var viewToRemove = viewList.Find(view => view.UserId == userId); + if (viewToRemove != null) + { + viewList.Remove(viewToRemove); + Views = viewList; + } + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/UserOrganizationsViews.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/UserOrganizationsViews.cs new file mode 100644 index 000000000..13b5a5f84 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/UserOrganizationsViews.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Organizations.Core.Entities +{ + public class UserOrganizationsViews + { + public Guid UserId { get; private set; } + public IEnumerable Views { get; private set; } + + public UserOrganizationsViews(Guid userProfileId, IEnumerable views) + { + UserId = userProfileId; + Views = views ?? new List(); + } + + public void AddView(Guid organizationId, DateTime date, string ipAddress, string deviceType, string operatingSystem) + { + var viewList = new List(Views) + { + new UserView(organizationId, date, ipAddress, deviceType, operatingSystem) + }; + Views = viewList; + } + + public void RemoveView(Guid organizationId) + { + var viewList = new List(Views); + var viewToRemove = viewList.Find(view => view.OrganizationId == organizationId); + if (viewToRemove != null) + { + viewList.Remove(viewToRemove); + Views = viewList; + } + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/UserView.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/UserView.cs new file mode 100644 index 000000000..25ff65882 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Entities/UserView.cs @@ -0,0 +1,22 @@ +using System; + +namespace MiniSpace.Services.Organizations.Core.Entities +{ + public class UserView + { + public Guid OrganizationId { get; private set; } + public DateTime Date { get; private set; } + public string IpAddress { get; private set; } + public string DeviceType { get; private set; } + public string OperatingSystem { get; private set; } + + public UserView(Guid organizationId, DateTime date, string ipAddress, string deviceType, string operatingSystem) + { + OrganizationId = organizationId; + Date = date; + IpAddress = ipAddress; + DeviceType = deviceType; + OperatingSystem = operatingSystem; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationViewsRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationViewsRepository.cs new file mode 100644 index 000000000..bb27e7881 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IOrganizationViewsRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Core.Repositories +{ + public interface IOrganizationViewsRepository + { + Task GetAsync(Guid organizationId); + Task AddAsync(OrganizationViews organizationViews); + Task UpdateAsync(OrganizationViews organizationViews); + Task DeleteAsync(Guid organizationId); + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IUserOrganizationViewsRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IUserOrganizationViewsRepository.cs new file mode 100644 index 000000000..c3c0d99fe --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Core/Repositories/IUserOrganizationViewsRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Organizations.Core.Entities; + +namespace MiniSpace.Services.Organizations.Core.Repositories +{ + public interface IUserOrganizationViewsRepository + { + Task GetAsync(Guid userId); + Task AddAsync(UserOrganizationsViews userOrganizationsViews); + Task UpdateAsync(UserOrganizationsViews userOrganizationsViews); + Task DeleteAsync(Guid userId); + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizationRequestsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizationRequestsHandler.cs new file mode 100644 index 000000000..92308c268 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizationRequestsHandler.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; +using MiniSpace.Services.Organizations.Application.Queries; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories; +using MongoDB.Driver; + +namespace MiniSpace.Services.Organizations.Infrastructure.Queries.Handlers +{ + public class GetPaginatedOrganizationRequestsHandler : IQueryHandler> + { + private readonly IOrganizationRequestsRepository _requestsRepository; + + public GetPaginatedOrganizationRequestsHandler(IOrganizationRequestsRepository requestsRepository) + { + _requestsRepository = requestsRepository; + } + + public async Task> HandleAsync(GetPaginatedOrganizationRequests query, CancellationToken cancellationToken) + { + // Retrieve all requests for the organization + var requests = await _requestsRepository.GetRequestsAsync(query.OrganizationId); + + if (!requests.Any()) + { + return new MiniSpace.Services.Organizations.Application.DTO.PagedResult(Enumerable.Empty(), query.Page, query.PageSize, 0); + } + + // Count total items + var totalItems = requests.Count(); + + // Paginate the requests + var paginatedRequests = requests + .Skip((query.Page - 1) * query.PageSize) + .Take(query.PageSize) + .Select(r => new OrganizationRequestDto + { + RequestId = r.Id, + UserId = r.UserId, + RequestDate = r.RequestDate, + State = r.State.ToString(), + Reason = r.Reason + }) + .ToList(); + + // Return a paged result + return new MiniSpace.Services.Organizations.Application.DTO.PagedResult(paginatedRequests, query.Page, query.PageSize, totalItems); + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizationsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizationsHandler.cs index bfe49cdaf..f31a57baf 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizationsHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedOrganizationsHandler.cs @@ -1,5 +1,6 @@ using Convey.CQRS.Queries; using MiniSpace.Services.Organizations.Application.DTO; +using MiniSpace.Services.Organizations.Application.Queries; using MiniSpace.Services.Organizations.Core.Repositories; using MiniSpace.Services.Organizations.Infrastructure.Mongo.Documents; using MiniSpace.Services.Organizations.Infrastructure.Mongo.Repositories; @@ -9,7 +10,7 @@ using System.Threading; using System.Threading.Tasks; -namespace MiniSpace.Services.Organizations.Application.Queries.Handlers +namespace MiniSpace.Services.Organizations.Infrastructure.Queries.Handlers { public class GetPaginatedOrganizationsHandler : IQueryHandler> { @@ -114,7 +115,8 @@ private async Task ConvertToDtoAsync(OrganizationDocument organ City = organization.City, Telephone = organization.Telephone, Email = organization.Email, - Users = members?.Select(user => new UserDto(user)).ToList() + Users = members?.Select(user => new UserDto(user)).ToList(), + Settings = organization.Settings != null ? new OrganizationSettingsDto(organization.Settings) : null }; } } diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedUserOrganizationsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedUserOrganizationsHandler.cs new file mode 100644 index 000000000..616b84d93 --- /dev/null +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetPaginatedUserOrganizationsHandler.cs @@ -0,0 +1,79 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Organizations.Application.DTO; +using MiniSpace.Services.Organizations.Core.Entities; +using MiniSpace.Services.Organizations.Core.Repositories; +using MiniSpace.Services.Organizations.Application.Queries; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Organizations.Infrastructure.Mongo.Queries.Handlers +{ + public class GetPaginatedUserOrganizationsHandler : IQueryHandler> + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationMembersRepository _organizationMembersRepository; + + public GetPaginatedUserOrganizationsHandler( + IOrganizationRepository organizationRepository, + IOrganizationMembersRepository organizationMembersRepository) + { + _organizationRepository = organizationRepository; + _organizationMembersRepository = organizationMembersRepository; + } + + public async Task> HandleAsync(GetPaginatedUserOrganizations query, CancellationToken cancellationToken) + { + var organizations = await _organizationRepository.GetOrganizationsByUserAsync(query.UserId); + + var matchedOrganizations = new List(); + + foreach (var org in organizations) + { + var organizationDto = await ConvertToDtoAsync(org); + matchedOrganizations.Add(organizationDto); + + if (org.SubOrganizations != null && org.SubOrganizations.Any()) + { + foreach (var subOrg in org.SubOrganizations) + { + var subOrganizationDto = await ConvertToDtoAsync(subOrg); + matchedOrganizations.Add(subOrganizationDto); + } + } + } + + var totalItems = matchedOrganizations.Count; + var paginatedOrganizations = matchedOrganizations + .Skip((query.Page - 1) * query.PageSize) + .Take(query.PageSize) + .ToList(); + + return new MiniSpace.Services.Organizations.Application.DTO.PagedResult(paginatedOrganizations, query.Page, query.PageSize, totalItems); + } + + private async Task ConvertToDtoAsync(Organization organization) + { + var members = await _organizationMembersRepository.GetMembersAsync(organization.Id); + + return new OrganizationDto + { + Id = organization.Id, + Name = organization.Name, + Description = organization.Description, + ImageUrl = organization.ImageUrl, + BannerUrl = organization.BannerUrl, + OwnerId = organization.OwnerId, + DefaultRoleName = organization.DefaultRoleName, + Address = organization.Address, + Country = organization.Country, + City = organization.City, + Telephone = organization.Telephone, + Email = organization.Email, + Users = members?.Select(user => new UserDto(user)).ToList(), + Settings = organization.Settings != null ? new OrganizationSettingsDto(organization.Settings) : null // Include settings + }; + } + } +} diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetUserFollowOrganizationsHandler.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetUserFollowOrganizationsHandler.cs index 30a4e4571..0b05cb375 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetUserFollowOrganizationsHandler.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Queries/Handlers/GetUserFollowOrganizationsHandler.cs @@ -34,6 +34,11 @@ public async Task> HandleAsync(GetUserF var organizationIds = await _userOrganizationsRepository.GetUserOrganizationsAsync(query.UserId); var organizationDetailsList = new List(); + if (organizationIds == null || !organizationIds.Any()) + { + return Enumerable.Empty(); + } + foreach (var organizationId in organizationIds) { var organization = await _organizationRepository.GetAsync(organizationId); diff --git a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMembersMongoRepository.cs b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMembersMongoRepository.cs index 20e2d5e96..db8a2e443 100644 --- a/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMembersMongoRepository.cs +++ b/MiniSpace.Services.Organizations/src/MiniSpace.Services.Organizations.Infrastructure/Mongo/Repositories/OrganizationMembersMongoRepository.cs @@ -86,5 +86,6 @@ public async Task DeleteMemberAsync(Guid organizationId, Guid memberId) } } } + } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs index 4561f84a3..a7b91bf1b 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Api/Program.cs @@ -14,6 +14,7 @@ using MiniSpace.Services.Posts.Application; using MiniSpace.Services.Posts.Application.Commands; using MiniSpace.Services.Posts.Application.Dto; +using MiniSpace.Services.Posts.Application.DTO; using MiniSpace.Services.Posts.Application.Queries; using MiniSpace.Services.Posts.Application.Services; using MiniSpace.Services.Posts.Core.Wrappers; @@ -39,7 +40,11 @@ public static async Task Main(string[] args) .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) .Get("posts/{postId}") .Get>("posts/search") + .Get>("posts/users/{userId}/feed") .Get>("posts/organizer/{organizerId}") + .Post("posts/{postId}/view", + afterDispatch: (cmd, ctx) => ctx.Response.NoContent()) + .Get>("posts/users/{userId}/views") .Put("posts/{postId}") .Delete("posts/{postId}") .Post("posts", diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/ViewPostHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/ViewPostHandler.cs new file mode 100644 index 000000000..5beee8aec --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/ViewPostHandler.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; +using Microsoft.Extensions.Logging; + +namespace MiniSpace.Services.Posts.Application.Commands.Handlers +{ + public class ViewPostHandler : ICommandHandler + { + private readonly IPostsUserViewsRepository _postsUserViewsRepository; + private readonly IPostRepository _postRepository; + private readonly ILogger _logger; + + public ViewPostHandler( + IPostsUserViewsRepository postsUserViewsRepository, + IPostRepository postRepository, + ILogger logger) + { + _postsUserViewsRepository = postsUserViewsRepository; + _postRepository = postRepository; + _logger = logger; + } + + public async Task HandleAsync(ViewPost command, CancellationToken cancellationToken) + { + var postExists = await _postRepository.ExistsAsync(command.PostId); + if (!postExists) + { + _logger.LogWarning($"Post with ID {command.PostId} not found."); + return; + } + + var userViews = await _postsUserViewsRepository.GetAsync(command.UserId); + if (userViews == null) + { + userViews = new PostsViews(command.UserId, Enumerable.Empty()); + } + + var existingView = userViews.Views.FirstOrDefault(v => v.PostId == command.PostId); + if (existingView != null) + { + userViews.RemoveView(command.PostId); + } + + userViews.AddView(command.PostId, DateTime.UtcNow); + + await _postsUserViewsRepository.UpdateAsync(userViews); + + _logger.LogInformation($"User {command.UserId} viewed post {command.PostId}."); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/ViewPost.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/ViewPost.cs new file mode 100644 index 000000000..494c2cbe0 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/ViewPost.cs @@ -0,0 +1,17 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Posts.Application.Commands +{ + public class ViewPost : ICommand + { + public Guid UserId { get; } + public Guid PostId { get; } + + public ViewPost(Guid userId, Guid postId) + { + UserId = userId; + PostId = postId; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/EducationDto.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/EducationDto.cs new file mode 100644 index 000000000..d6ce3e726 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/EducationDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Posts.Application.Dto +{ + [ExcludeFromCodeCoverage] + public class EducationDto + { + public string InstitutionName { get; set; } + public string Degree { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/FriendDto.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/FriendDto.cs new file mode 100644 index 000000000..af175afc8 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/FriendDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Posts.Application.Dto +{ + public class FriendDto + { + public Guid Id { get; set; } + public Guid StudentId { get; set; } + public Guid FriendId { get; set; } + public DateTime CreatedAt { get; set; } + public string FriendState { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/UserDto.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/UserDto.cs new file mode 100644 index 000000000..60ed82e35 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/UserDto.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + + +namespace MiniSpace.Services.Posts.Application.Dto +{ + [ExcludeFromCodeCoverage] + public class UserDto + { + public Guid Id { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string ProfileImageUrl { get; set; } + public string Description { get; set; } + public DateTime? DateOfBirth { get; set; } + public bool EmailNotifications { get; set; } + public bool IsBanned { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public string ContactEmail { get; set; } + public string BannerUrl { get; set; } + public string PhoneNumber { get; set; } + public IEnumerable Languages { get; set; } + public IEnumerable Interests { get; set; } + public IEnumerable Education { get; set; } + public IEnumerable Work { get; set; } + public bool IsTwoFactorEnabled { get; set; } + public string TwoFactorSecret { get; set; } + public IEnumerable InterestedInEvents { get; set; } + public IEnumerable SignedUpEvents { get; set; } + public string Country { get; set; } + public string City { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/UserFriendsDto.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/UserFriendsDto.cs new file mode 100644 index 000000000..8b4cdeae7 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/UserFriendsDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Posts.Application.Dto +{ + public interface UserFriendsDto + { + public Guid StudentId { get; set; } + public List Friends { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/UserPostsViewsDto.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/UserPostsViewsDto.cs new file mode 100644 index 000000000..c030f30ab --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/UserPostsViewsDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Posts.Application.DTO +{ + public class UserPostsViewsDto + { + public Guid UserId { get; set; } + public IEnumerable Views { get; set; } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/ViewDto.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/ViewDto.cs new file mode 100644 index 000000000..1f307795a --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/ViewDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Posts.Application.DTO +{ + public class ViewDto + { + public Guid PostId { get; set; } + public DateTime Date { get; set; } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/WorkDto.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/WorkDto.cs new file mode 100644 index 000000000..edaf90f6a --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/WorkDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Posts.Application.Dto +{ + [ExcludeFromCodeCoverage] + public class WorkDto + { + public string Company { get; set; } + public string Position { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/CommentCreated.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/CommentCreated.cs new file mode 100644 index 000000000..ed6d6a435 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/CommentCreated.cs @@ -0,0 +1,37 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using System; + +namespace MiniSpace.Services.Posts.Application.Events.External +{ + [Message("comments")] + public class CommentCreated : IEvent + { + public Guid CommentId { get; } + public Guid ContextId { get; } + public string CommentContext { get; } + public Guid UserId { get; } + public Guid ParentId { get; } + public string TextContent { get; } + public DateTime CreatedAt { get; } + public DateTime LastUpdatedAt { get; } + public int RepliesCount { get; } + public bool IsDeleted { get; } + + public CommentCreated(Guid commentId, Guid contextId, string commentContext, Guid userId, + Guid parentId, string textContent, DateTime createdAt, + DateTime lastUpdatedAt, int repliesCount, bool isDeleted) + { + CommentId = commentId; + ContextId = contextId; + CommentContext = commentContext; + UserId = userId; + ParentId = parentId; + TextContent = textContent; + CreatedAt = createdAt; + LastUpdatedAt = lastUpdatedAt; + RepliesCount = repliesCount; + IsDeleted = isDeleted; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/CommentCreatedHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/CommentCreatedHandler.cs new file mode 100644 index 000000000..473fbece3 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/CommentCreatedHandler.cs @@ -0,0 +1,46 @@ +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using MiniSpace.Services.Posts.Application.Events.External; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; + +namespace MiniSpace.Services.Posts.Application.Events.External.Handlers +{ + public class CommentCreatedHandler : IEventHandler + { + private readonly IUserCommentsHistoryRepository _userCommentsHistoryRepository; + + public CommentCreatedHandler(IUserCommentsHistoryRepository userCommentsHistoryRepository) + { + _userCommentsHistoryRepository = userCommentsHistoryRepository; + } + + public async Task HandleAsync(CommentCreated @event, CancellationToken cancellationToken = default) + { + + var eventJson = JsonSerializer.Serialize(@event, new JsonSerializerOptions + { + WriteIndented = true // Optional: For pretty-printing the JSON + }); + Console.WriteLine("Received CommentCreated event:"); + Console.WriteLine(eventJson); + + var comment = new Comment( + @event.CommentId, + @event.ContextId, + @event.CommentContext, + @event.UserId, + @event.ParentId, + @event.TextContent, + @event.CreatedAt, + @event.LastUpdatedAt, + @event.RepliesCount, + @event.IsDeleted + ); + + await _userCommentsHistoryRepository.SaveCommentAsync(@event.UserId, comment); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/ReactionCreatedHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/ReactionCreatedHandler.cs new file mode 100644 index 000000000..38612beeb --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/ReactionCreatedHandler.cs @@ -0,0 +1,42 @@ +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using MiniSpace.Services.Posts.Application.Events.External; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; + +namespace MiniSpace.Services.Posts.Application.Events.External.Handlers +{ + public class ReactionCreatedHandler : IEventHandler + { + private readonly IUserReactionsHistoryRepository _userReactionsHistoryRepository; + + public ReactionCreatedHandler(IUserReactionsHistoryRepository userReactionsHistoryRepository) + { + _userReactionsHistoryRepository = userReactionsHistoryRepository; + } + + public async Task HandleAsync(ReactionCreated @event, CancellationToken cancellationToken = default) + { + var eventJson = JsonSerializer.Serialize(@event, new JsonSerializerOptions + { + WriteIndented = true + }); + Console.WriteLine("Received ReactionCreated event:"); + Console.WriteLine(eventJson); + + var reaction = Reaction.Create( + @event.ReactionId, + @event.UserId, + @event.ReactionType, + @event.ContentId, + @event.ContentType, + @event.TargetType + ); + + await _userReactionsHistoryRepository.SaveReactionAsync(@event.UserId, reaction); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/ReactionCreated.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/ReactionCreated.cs new file mode 100644 index 000000000..87d2283e9 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/ReactionCreated.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Posts.Application.Events.External +{ + [Message("reactions")] + public class ReactionCreated : IEvent + { + public Guid ReactionId { get; } + public Guid UserId { get; } + public Guid ContentId { get; } + public string ContentType { get; } + public string ReactionType { get; } + public string TargetType { get; } + + public ReactionCreated(Guid reactionId, Guid userId, Guid contentId, string contentType, string reactionType, string targetType) + { + ReactionId = reactionId; + UserId = userId; + ContentId = contentId; + ContentType = contentType; + ReactionType = reactionType; + TargetType = targetType; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetUserFeed.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetUserFeed.cs new file mode 100644 index 000000000..aa49850f9 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetUserFeed.cs @@ -0,0 +1,28 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Convey.CQRS.Queries; +using MiniSpace.Services.Posts.Application.Dto; +using MiniSpace.Services.Posts.Core.Wrappers; + +namespace MiniSpace.Services.Posts.Application.Queries +{ + [ExcludeFromCodeCoverage] + public class GetUserFeed : IQuery> + { + public Guid UserId { get; set; } + public int PageNumber { get; set; } = 1; + public int PageSize { get; set; } = 10; + public string SortBy { get; set; } = "PublishDate"; + public string Direction { get; set; } = "asc"; + + public GetUserFeed(Guid userId, int pageNumber = 1, int pageSize = 10, + string sortBy = "PublishDate", string direction = "asc") + { + UserId = userId; + PageNumber = pageNumber; + PageSize = pageSize; + SortBy = sortBy; + Direction = direction; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetUserPostViews.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetUserPostViews.cs new file mode 100644 index 000000000..98dbb78cb --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Queries/GetUserPostViews.cs @@ -0,0 +1,22 @@ +using System; +using Convey.CQRS.Queries; +using MiniSpace.Services.Posts.Application.Dto; +using MiniSpace.Services.Posts.Application.DTO; +using MiniSpace.Services.Posts.Core.Wrappers; + +namespace MiniSpace.Services.Posts.Application.Queries +{ + public class GetUserPostViews : IQuery> + { + public Guid UserId { get; } + public int PageNumber { get; } + public int PageSize { get; } + + public GetUserPostViews(Guid userId, int pageNumber = 1, int pageSize = 10) + { + UserId = userId; + PageNumber = pageNumber; + PageSize = pageSize; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/Clients/IFriendsServiceClient.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/Clients/IFriendsServiceClient.cs new file mode 100644 index 000000000..d4fc9f008 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/Clients/IFriendsServiceClient.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MiniSpace.Services.Posts.Application.Dto; + +namespace MiniSpace.Services.Posts.Application.Services.Clients +{ + public interface IFriendsServiceClient + { + Task> GetAsync(Guid userId); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/Clients/IStudentsServiceClient.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/Clients/IStudentsServiceClient.cs index 849818345..0be4f83a0 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/Clients/IStudentsServiceClient.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/Clients/IStudentsServiceClient.cs @@ -1,9 +1,12 @@ -using MiniSpace.Services.Posts.Application.Dto; +using System; +using System.Threading.Tasks; +using MiniSpace.Services.Posts.Application.Dto; namespace MiniSpace.Services.Posts.Application.Services.Clients { public interface IStudentsServiceClient { Task GetAsync(Guid id); + Task GetStudentByIdAsync(Guid studentId); } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/IPostRecommendationService.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/IPostRecommendationService.cs new file mode 100644 index 000000000..8f7a5f11d --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Services/IPostRecommendationService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Posts.Application.Dto; + +namespace MiniSpace.Services.Posts.Application.Services +{ + public interface IPostRecommendationService + { + Task> RankPostsByUserInterestAsync(Guid userId, + IEnumerable posts, IDictionary userInterests); + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Comment.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Comment.cs new file mode 100644 index 000000000..408987a4d --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Comment.cs @@ -0,0 +1,59 @@ +using System; + +namespace MiniSpace.Services.Posts.Core.Entities +{ + public class Comment + { + public Guid Id { get; private set; } + public Guid ContextId { get; private set; } + public string CommentContext { get; private set; } + public Guid UserId { get; private set; } + public Guid ParentId { get; private set; } + public string TextContent { get; private set; } + public DateTime CreatedAt { get; private set; } + public DateTime LastUpdatedAt { get; private set; } + public int RepliesCount { get; private set; } + public bool IsDeleted { get; private set; } + + public Comment(Guid id, Guid contextId, string commentContext, Guid userId, + Guid parentId, string textContent, DateTime createdAt, + DateTime lastUpdatedAt, int repliesCount, bool isDeleted) + { + Id = id; + ContextId = contextId; + CommentContext = commentContext; + UserId = userId; + ParentId = parentId; + TextContent = textContent; + CreatedAt = createdAt; + LastUpdatedAt = lastUpdatedAt; + RepliesCount = repliesCount; + IsDeleted = isDeleted; + } + + public void UpdateText(string newText, DateTime updatedAt) + { + TextContent = newText; + LastUpdatedAt = updatedAt; + } + + public void MarkAsDeleted() + { + IsDeleted = true; + TextContent = "[deleted]"; + } + + public void IncrementRepliesCount() + { + RepliesCount++; + } + + public void DecrementRepliesCount() + { + if (RepliesCount > 0) + { + RepliesCount--; + } + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs index 0dc5b9df5..2870b48f0 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs @@ -18,7 +18,7 @@ public class Post : AggregateRoot public DateTime CreatedAt { get; private set; } public DateTime? UpdatedAt { get; private set; } public PostContext Context { get; private set; } - public VisibilityStatus Visibility { get; private set; } // New visibility status property + public VisibilityStatus Visibility { get; private set; } public Post(Guid id, Guid? userId, Guid? organizationId, Guid? eventId, string textContent, IEnumerable mediaFiles, DateTime createdAt, State state, PostContext context, DateTime? publishDate, @@ -166,7 +166,6 @@ public void RemoveMediaFile(string mediaFileUrl, DateTime now) MediaFiles = MediaFiles.Where(mf => mf != mediaFileUrl).ToList(); UpdatedAt = now; - // Raise an event if necessary (e.g., PostMediaFileRemovedEvent) } private static void CheckTextContent(AggregateId id, string textContent) diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/PostsViews.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/PostsViews.cs new file mode 100644 index 000000000..af833c8ff --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/PostsViews.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Services.Posts.Core.Entities +{ + public class PostsViews + { + public Guid UserId { get; private set; } + public IEnumerable Views { get; private set; } + + public PostsViews(Guid userId, IEnumerable views) + { + UserId = userId; + Views = views ?? new List(); + } + + public void AddView(Guid postId, DateTime date) + { + var viewList = new List(Views) + { + new View(postId, date) + }; + Views = viewList; + } + + public void RemoveView(Guid postId) + { + var viewList = new List(Views); + var viewToRemove = viewList.Find(view => view.PostId == postId); + if (viewToRemove != null) + { + viewList.Remove(viewToRemove); + Views = viewList; + } + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Reaction.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Reaction.cs new file mode 100644 index 000000000..9237c83d4 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Reaction.cs @@ -0,0 +1,58 @@ +using System; + +namespace MiniSpace.Services.Posts.Core.Entities +{ + public class Reaction + { + public Guid Id { get; private set; } + public Guid UserId { get; private set; } + public Guid ContentId { get; private set; } + public string Type { get; private set; } + public string ContentType { get; private set; } + public string TargetType { get; private set; } + public DateTime CreatedAt { get; private set; } + + private Reaction() { } + + private Reaction(Guid id, Guid userId, string type, Guid contentId, string contentType, string targetType) + { + if (string.IsNullOrWhiteSpace(type)) + { + throw new ArgumentException("Reaction type cannot be null or empty.", nameof(type)); + } + + if (string.IsNullOrWhiteSpace(contentType)) + { + throw new ArgumentException("Content type cannot be null or empty.", nameof(contentType)); + } + + if (string.IsNullOrWhiteSpace(targetType)) + { + throw new ArgumentException("Target type cannot be null or empty.", nameof(targetType)); + } + + Id = id != Guid.Empty ? id : throw new ArgumentException("Reaction ID cannot be empty.", nameof(id)); + UserId = userId != Guid.Empty ? userId : throw new ArgumentException("User ID cannot be empty.", nameof(userId)); + ContentId = contentId != Guid.Empty ? contentId : throw new ArgumentException("Content ID cannot be empty.", nameof(contentId)); + Type = type; + ContentType = contentType; + TargetType = targetType; + CreatedAt = DateTime.UtcNow; + } + + public static Reaction Create(Guid id, Guid userId, string type, Guid contentId, string contentType, string targetType) + { + return new Reaction(id, userId, type, contentId, contentType, targetType); + } + + public void UpdateReactionType(string newType) + { + if (string.IsNullOrWhiteSpace(newType)) + { + throw new ArgumentException("Reaction type cannot be null or empty.", nameof(newType)); + } + + Type = newType; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/View.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/View.cs new file mode 100644 index 000000000..0fcbb0d57 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/View.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Posts.Core.Entities +{ + public class View + { + public Guid PostId { get; private set; } + public DateTime Date { get; private set; } + + public View(Guid postId, DateTime date) + { + PostId = postId; + Date = date; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IPostsUserViewsRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IPostsUserViewsRepository.cs new file mode 100644 index 000000000..aaf0548fc --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IPostsUserViewsRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Core.Repositories +{ + public interface IPostsUserViewsRepository + { + Task GetAsync(Guid userId); + Task AddAsync(PostsViews postsViews); + Task UpdateAsync(PostsViews postsViews); + Task DeleteAsync(Guid userId); + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IUserCommentsHistoryRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IUserCommentsHistoryRepository.cs new file mode 100644 index 000000000..cfc5b0479 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IUserCommentsHistoryRepository.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Wrappers; + +namespace MiniSpace.Services.Posts.Core.Repositories +{ + public interface IUserCommentsHistoryRepository + { + Task SaveCommentAsync(Guid userId, Comment comment); + + Task> GetUserCommentsAsync(Guid userId); + + Task> GetUserCommentsPagedAsync(Guid userId, int pageNumber, int pageSize); + + Task DeleteCommentAsync(Guid userId, Guid commentId); + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IUserReactionsHistoryRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IUserReactionsHistoryRepository.cs new file mode 100644 index 000000000..d2d65ae0f --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IUserReactionsHistoryRepository.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Wrappers; + +namespace MiniSpace.Services.Posts.Core.Repositories +{ + public interface IUserReactionsHistoryRepository + { + Task SaveReactionAsync(Guid userId, Reaction reaction); + + Task> GetUserReactionsAsync(Guid userId); + + Task> GetUserReactionsPagedAsync(Guid userId, int pageNumber, int pageSize); + + Task DeleteReactionAsync(Guid userId, Guid reactionId); + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs index f57c4672a..1e96b74bc 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Extensions.cs @@ -42,6 +42,8 @@ using System.Diagnostics.CodeAnalysis; using MiniSpace.Services.Events.Infrastructure.Services.Clients; using MiniSpace.Services.Posts.Application.Services.Clients; +using Microsoft.ML; +using MiniSpace.Services.Events.Infrastructure.Mongo.Repositories; namespace MiniSpace.Services.Posts.Infrastructure { @@ -56,10 +58,18 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddSingleton(new MLContext()); + builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(ctx => ctx.GetRequiredService().Create()); builder.Services.TryDecorate(typeof(ICommandHandler<>), typeof(OutboxCommandHandlerDecorator<>)); @@ -86,6 +96,9 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) .AddMongoRepository("user_events_posts") .AddMongoRepository("user_posts") .AddMongoRepository("posts") + .AddMongoRepository("user_comments_history") + .AddMongoRepository("user_reactions_history") + .AddMongoRepository("user_views") .AddWebApiSwaggerDocs() .AddCertificateAuthentication() .AddSecurity(); @@ -106,7 +119,9 @@ public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app .SubscribeCommand() .SubscribeCommand() .SubscribeCommand() - .SubscribeEvent(); + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent(); return app; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/MiniSpace.Services.Posts.Infrastructure.csproj b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/MiniSpace.Services.Posts.Infrastructure.csproj index 34640728d..f7e40f538 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/MiniSpace.Services.Posts.Infrastructure.csproj +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/MiniSpace.Services.Posts.Infrastructure.csproj @@ -30,6 +30,7 @@ + diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/CommentDocument.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/CommentDocument.cs new file mode 100644 index 000000000..f88b51164 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/CommentDocument.cs @@ -0,0 +1,19 @@ +using System; +using Convey.Types; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents +{ + public class CommentDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid ContextId { get; set; } + public string CommentContext { get; set; } + public Guid UserId { get; set; } + public Guid ParentId { get; set; } + public string TextContent { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime LastUpdatedAt { get; set; } + public int RepliesCount { get; set; } + public bool IsDeleted { get; set; } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/Extensions.cs index 2a40e02a2..18f83a151 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/Extensions.cs @@ -22,6 +22,8 @@ public static Post AsEntity(this PostDocument document) document.Visibility, document.UpdatedAt); + + public static PostDocument AsDocument(this Post entity) => new PostDocument() { @@ -54,5 +56,112 @@ public static PostDto AsDto(this PostDocument document) PublishDate = document.PublishDate, Visibility = document.Visibility.ToString().ToLowerInvariant() }; + + public static PostDto AsDto(this Post post) + { + return new PostDto + { + Id = post.Id, + UserId = post.UserId, + OrganizationId = post.OrganizationId, + EventId = post.EventId, + TextContent = post.TextContent, + MediaFiles = post.MediaFiles, + CreatedAt = post.CreatedAt, + UpdatedAt = post.UpdatedAt, + State = post.State.ToString().ToLowerInvariant(), + PublishDate = post.PublishDate, + Visibility = post.Visibility.ToString().ToLowerInvariant() + }; + } + + public static CommentDocument AsDocument(this Comment comment) + { + return new CommentDocument + { + Id = comment.Id, + ContextId = comment.ContextId, + CommentContext = comment.CommentContext, + UserId = comment.UserId, + ParentId = comment.ParentId, + TextContent = comment.TextContent, + CreatedAt = comment.CreatedAt, + LastUpdatedAt = comment.LastUpdatedAt, + RepliesCount = comment.RepliesCount, + IsDeleted = comment.IsDeleted + }; + } + + public static Comment AsEntity(this CommentDocument document) + { + return new Comment( + document.Id, + document.ContextId, + document.CommentContext, + document.UserId, + document.ParentId, + document.TextContent, + document.CreatedAt, + document.LastUpdatedAt, + document.RepliesCount, + document.IsDeleted + ); + } + + public static UserCommentsDocument AsDocument(this IEnumerable comments, Guid userId) + { + return new UserCommentsDocument + { + Id = Guid.NewGuid(), + UserId = userId, + Comments = comments.Select(comment => comment.AsDocument()).ToList() + }; + } + + public static IEnumerable AsEntities(this UserCommentsDocument document) + { + return document.Comments.Select(doc => doc.AsEntity()); + } + + public static ReactionDocument AsDocument(this Reaction reaction) + { + return new ReactionDocument + { + Id = reaction.Id, + UserId = reaction.UserId, + ContentId = reaction.ContentId, + ContentType = reaction.ContentType, + ReactionType = reaction.Type, + TargetType = reaction.TargetType, + CreatedAt = reaction.CreatedAt + }; + } + + public static Reaction AsEntity(this ReactionDocument document) + { + return Reaction.Create( + document.Id, + document.UserId, + document.ReactionType, + document.ContentId, + document.ContentType, + document.TargetType + ); + } + + public static UserReactionDocument AsDocument(this IEnumerable reactions, Guid userId) + { + return new UserReactionDocument + { + Id = Guid.NewGuid(), + UserId = userId, + Reactions = reactions.Select(reaction => reaction.AsDocument()).ToList() + }; + } + + public static IEnumerable AsEntities(this UserReactionDocument document) + { + return document.Reactions.Select(doc => doc.AsEntity()); + } } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostsViewsExtensions.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostsViewsExtensions.cs new file mode 100644 index 000000000..b3b506e21 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostsViewsExtensions.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using MiniSpace.Services.Posts.Application.DTO; +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents +{ + public static class PostsViewsExtensions + { + public static UserPostsViewsDto AsDto(this UserPostsViewsDocument document) + { + return new UserPostsViewsDto + { + UserId = document.UserId, + Views = document.Views.Select(v => new ViewDto + { + PostId = v.PostId, + Date = v.Date + }) + }; + } + + public static UserPostsViewsDocument AsDocument(this PostsViews entity) + { + return new UserPostsViewsDocument + { + Id = Guid.NewGuid(), + UserId = entity.UserId, + Views = entity.Views.Select(ViewDocument.FromEntity).ToList() + }; + } + + public static PostsViews AsEntity(this UserPostsViewsDocument document) + { + return new PostsViews( + document.UserId, + document.Views.Select(v => new View(v.PostId, v.Date)) + ); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/ReactionDocument.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/ReactionDocument.cs new file mode 100644 index 000000000..6e4b50a49 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/ReactionDocument.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents +{ + public class ReactionDocument + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + public Guid ContentId { get; set; } + public string ContentType { get; set; } + public string ReactionType { get; set; } + public string TargetType { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/UserCommentsDocument.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/UserCommentsDocument.cs new file mode 100644 index 000000000..bdd649a2c --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/UserCommentsDocument.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using Convey.Types; +using MongoDB.Bson.Serialization.Attributes; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents +{ + public class UserCommentsDocument : IIdentifiable + { + [BsonId] + [BsonRepresentation(MongoDB.Bson.BsonType.String)] + public Guid Id { get; set; } + public Guid UserId { get; set; } + public IEnumerable Comments { get; set; } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/UserPostsViewsDocument.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/UserPostsViewsDocument.cs new file mode 100644 index 000000000..923215369 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/UserPostsViewsDocument.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Convey.Types; +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents +{ + public class UserPostsViewsDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + public List Views { get; set; } = new List(); + + public static UserPostsViewsDocument FromEntity(PostsViews eventsViews) + { + return new UserPostsViewsDocument + { + Id = Guid.NewGuid(), + UserId = eventsViews.UserId, + Views = new List(eventsViews.Views.Select(ViewDocument.FromEntity)) + }; + } + + public PostsViews ToEntity() + { + return new PostsViews(UserId, Views.Select(view => view.ToEntity())); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/UserReactionDocument.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/UserReactionDocument.cs new file mode 100644 index 000000000..190bef22e --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/UserReactionDocument.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using Convey.Types; +using MongoDB.Bson.Serialization.Attributes; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents +{ + public class UserReactionDocument : IIdentifiable + { + [BsonId] + [BsonRepresentation(MongoDB.Bson.BsonType.String)] + public Guid Id { get; set; } + public Guid UserId { get; set; } + public IEnumerable Reactions { get; set; } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/ViewDocument.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/ViewDocument.cs new file mode 100644 index 000000000..279f3aa25 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/ViewDocument.cs @@ -0,0 +1,28 @@ +using System; +using Convey.Types; +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Documents +{ + public class ViewDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid PostId { get; set; } + public DateTime Date { get; set; } + + public static ViewDocument FromEntity(View view) + { + return new ViewDocument + { + Id = Guid.NewGuid(), + PostId = view.PostId, + Date = view.Date + }; + } + + public View ToEntity() + { + return new View(PostId, Date); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostHandler.cs index 4e88709a2..35a56b605 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostHandler.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetPostHandler.cs @@ -1,8 +1,12 @@ using System.Diagnostics.CodeAnalysis; using Convey.CQRS.Queries; -using Convey.Persistence.MongoDB; using MiniSpace.Services.Posts.Application.Dto; using MiniSpace.Services.Posts.Application.Queries; +using MiniSpace.Services.Posts.Core.Repositories; +using System; +using System.Threading; +using System.Threading.Tasks; +using MiniSpace.Services.Posts.Core.Entities; using MiniSpace.Services.Posts.Infrastructure.Mongo.Documents; namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Queries.Handlers @@ -10,18 +14,52 @@ namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Queries.Handlers [ExcludeFromCodeCoverage] public class GetPostHandler : IQueryHandler { - private readonly IMongoRepository _repository; + private readonly IOrganizationEventPostRepository _organizationEventPostRepository; + private readonly IOrganizationPostRepository _organizationPostRepository; + private readonly IUserEventPostRepository _userEventPostRepository; + private readonly IUserPostRepository _userPostRepository; - public GetPostHandler(IMongoRepository repository) + public GetPostHandler( + IOrganizationEventPostRepository organizationEventPostRepository, + IOrganizationPostRepository organizationPostRepository, + IUserEventPostRepository userEventPostRepository, + IUserPostRepository userPostRepository) { - _repository = repository; + _organizationEventPostRepository = organizationEventPostRepository; + _organizationPostRepository = organizationPostRepository; + _userEventPostRepository = userEventPostRepository; + _userPostRepository = userPostRepository; } public async Task HandleAsync(GetPost query, CancellationToken cancellationToken) { - var post = await _repository.GetAsync(query.PostId); + Post post = null; - return post?.AsDto(); + post = await _organizationEventPostRepository.GetAsync(query.PostId); + if (post != null) + { + return post.AsDto(); + } + + post = await _organizationPostRepository.GetAsync(query.PostId); + if (post != null) + { + return post.AsDto(); + } + + post = await _userEventPostRepository.GetAsync(query.PostId); + if (post != null) + { + return post.AsDto(); + } + + post = await _userPostRepository.GetAsync(query.PostId); + if (post != null) + { + return post.AsDto(); + } + + return null; } } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetUserFeedHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetUserFeedHandler.cs new file mode 100644 index 000000000..2d0ed7d7f --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetUserFeedHandler.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Posts.Application.Dto; +using MiniSpace.Services.Posts.Application.Queries; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Core.Wrappers; +using Microsoft.Extensions.Logging; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Requests; +using Newtonsoft.Json; +using MiniSpace.Services.Posts.Application.Services.Clients; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Queries.Handlers +{ + public class GetUserFeedHandler : IQueryHandler> + { + private readonly IPostsService _postsService; + private readonly IUserCommentsHistoryRepository _userCommentsHistoryRepository; + private readonly IUserReactionsHistoryRepository _userReactionsHistoryRepository; + private readonly IPostRecommendationService _postRecommendationService; + private readonly IStudentsServiceClient _studentsServiceClient; + private readonly ILogger _logger; + + public GetUserFeedHandler( + IPostsService postsService, + IUserCommentsHistoryRepository userCommentsHistoryRepository, + IUserReactionsHistoryRepository userReactionsHistoryRepository, + IPostRecommendationService postRecommendationService, + IStudentsServiceClient studentsServiceClient, + ILogger logger) + { + _postsService = postsService; + _userCommentsHistoryRepository = userCommentsHistoryRepository; + _userReactionsHistoryRepository = userReactionsHistoryRepository; + _postRecommendationService = postRecommendationService; + _studentsServiceClient = studentsServiceClient; + _logger = logger; + } + + public async Task> HandleAsync(GetUserFeed query, CancellationToken cancellationToken) + { + _logger.LogInformation("Handling GetUserFeed query: {Query}", JsonConvert.SerializeObject(query)); + + var user = await _studentsServiceClient.GetStudentByIdAsync(query.UserId); + + var allPostsRequest = new BrowseRequest + { + SortBy = new List { query.SortBy }, + Direction = query.Direction + }; + + var allPostsResult = await _postsService.BrowsePostsAsync(allPostsRequest); + + if (allPostsResult == null || !allPostsResult.Items.Any()) + { + return new PagedResponse(Enumerable.Empty(), query.PageNumber, query.PageSize, 0); + } + + var userComments = await _userCommentsHistoryRepository.GetUserCommentsAsync(query.UserId); + var userReactions = await _userReactionsHistoryRepository.GetUserReactionsAsync(query.UserId); + + var userInterests = AnalyzeUserInteractions(user, userComments, userReactions); + + IEnumerable<(PostDto Post, double Score)> rankedPosts; + + if (!userInterests.Any() && !userComments.Any() && !userReactions.Any()) + { + _logger.LogInformation("User {UserId} has no interactions or defined interests, generating a random feed.", query.UserId); + rankedPosts = GenerateRandomFeed(allPostsResult.Items); + } + else + { + rankedPosts = await _postRecommendationService.RankPostsByUserInterestAsync(query.UserId, allPostsResult.Items, userInterests); + } + + var combinedPosts = CombineRankedAndUnrankedPosts(rankedPosts, allPostsResult.Items); + + var pagedPosts = combinedPosts + .Skip((query.PageNumber - 1) * query.PageSize) + .Take(query.PageSize) + .ToList(); + + _logger.LogInformation("User {UserId} feed generated with {PostCount} posts.", query.UserId, pagedPosts.Count); + + return new PagedResponse(pagedPosts, query.PageNumber, query.PageSize, combinedPosts.Count()); + } + + private IDictionary AnalyzeUserInteractions(UserDto user, IEnumerable userComments, IEnumerable userReactions) + { + var interestKeywords = new Dictionary(); + + if (user.Interests != null) + { + foreach (var interest in user.Interests) + { + if (interestKeywords.ContainsKey(interest)) + { + interestKeywords[interest] += 1; + } + else + { + interestKeywords[interest] = 1; + } + } + } + + if (user.Languages != null) + { + foreach (var language in user.Languages) + { + if (interestKeywords.ContainsKey(language)) + { + interestKeywords[language] += 1; + } + else + { + interestKeywords[language] = 1; + } + } + } + + foreach (var comment in userComments) + { + var words = comment.TextContent.Split(' ', StringSplitOptions.RemoveEmptyEntries); + foreach (var word in words) + { + if (interestKeywords.ContainsKey(word)) + { + interestKeywords[word] += 1; + } + else + { + interestKeywords[word] = 1; + } + } + } + + foreach (var reaction in userReactions) + { + var reactionType = reaction.Type; + if (interestKeywords.ContainsKey(reactionType)) + { + interestKeywords[reactionType] += 1; + } + else + { + interestKeywords[reactionType] = 1; + } + } + + var total = interestKeywords.Values.Sum(); + if (total > 0) + { + var normalizedInterests = interestKeywords.ToDictionary(kvp => kvp.Key, kvp => kvp.Value / total); + _logger.LogInformation("Inferred user interests with coefficients: {Interests}", string.Join(", ", normalizedInterests.Select(kvp => $"{kvp.Key}: {kvp.Value:F2}"))); + return normalizedInterests; + } + + return interestKeywords; + } + + private IEnumerable<(PostDto Post, double Score)> GenerateRandomFeed(IEnumerable allPosts) + { + var random = new Random(); + return allPosts + .OrderBy(p => random.NextDouble()) + .Select(p => (p, Score: random.NextDouble())) + .Take(100); + } + + + private IEnumerable CombineRankedAndUnrankedPosts(IEnumerable<(PostDto Post, double Score)> rankedPosts, IEnumerable allPosts) + { + var rankedPostIds = rankedPosts.Select(rp => rp.Post.Id).ToHashSet(); + var unrankedPosts = allPosts.Where(p => !rankedPostIds.Contains(p.Id)); + + return rankedPosts.Select(rp => rp.Post) + .Concat(unrankedPosts.OrderByDescending(p => p.PublishDate)); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetUserPostViewsHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetUserPostViewsHandler.cs new file mode 100644 index 000000000..f0cda20ee --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Queries/Handlers/GetUserPostViewsHandler.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Posts.Application.Dto; +using MiniSpace.Services.Posts.Application.DTO; +using MiniSpace.Services.Posts.Application.Queries; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Core.Wrappers; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Queries.Handlers +{ + public class GetUserPostViewsHandler : IQueryHandler> + { + private readonly IPostsUserViewsRepository _postsUserViewsRepository; + + public GetUserPostViewsHandler(IPostsUserViewsRepository postsUserViewsRepository) + { + _postsUserViewsRepository = postsUserViewsRepository; + } + + public async Task> HandleAsync(GetUserPostViews query, CancellationToken cancellationToken) + { + // Retrieve the user's post views + var userViews = await _postsUserViewsRepository.GetAsync(query.UserId); + + if (userViews == null || !userViews.Views.Any()) + { + return new PagedResponse(Enumerable.Empty(), query.PageNumber, query.PageSize, 0); + } + + // Paginate the views + var pagedViews = userViews.Views + .OrderByDescending(v => v.Date) + .Skip((query.PageNumber - 1) * query.PageSize) + .Take(query.PageSize) + .Select(v => new ViewDto { PostId = v.PostId, Date = v.Date }) + .ToList(); + + return new PagedResponse(pagedViews, query.PageNumber, query.PageSize, userViews.Views.Count()); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostsUserViewsMongoRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostsUserViewsMongoRepository.cs new file mode 100644 index 000000000..3a51102d1 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostsUserViewsMongoRepository.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading.Tasks; +using MongoDB.Driver; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Posts.Core.Entities; + +namespace MiniSpace.Services.Events.Infrastructure.Mongo.Repositories +{ + public class PostsUserViewsMongoRepository : IPostsUserViewsRepository + { + private readonly IMongoRepository _repository; + + public PostsUserViewsMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid userId) + { + var document = await _repository.GetAsync(x => x.UserId == userId); + return document?.ToEntity(); + } + + public async Task AddAsync(PostsViews postsViews) + { + var document = postsViews.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task UpdateAsync(PostsViews postsViews) + { + var document = postsViews.AsDocument(); + await _repository.UpdateAsync(document); + } + + public async Task DeleteAsync(Guid userId) + { + await _repository.DeleteAsync(x => x.UserId == userId); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/UserCommentsHistoryRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/UserCommentsHistoryRepository.cs new file mode 100644 index 000000000..860529725 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/UserCommentsHistoryRepository.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MongoDB.Driver; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Core.Wrappers; +using MiniSpace.Services.Posts.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Repositories +{ + public class UserCommentsHistoryRepository : IUserCommentsHistoryRepository + { + private readonly IMongoCollection _collection; + + public UserCommentsHistoryRepository(IMongoDatabase database) + { + _collection = database.GetCollection("user_comments_history"); + } + + public async Task SaveCommentAsync(Guid userId, Comment comment) + { + var filter = Builders.Filter.Eq(uc => uc.UserId, userId); + + var commentDocument = comment.AsDocument(); + + var update = Builders.Update.Combine( + Builders.Update.Push(uc => uc.Comments, commentDocument), + Builders.Update.SetOnInsert(uc => uc.UserId, userId), + Builders.Update.SetOnInsert(uc => uc.Id, Guid.NewGuid()) + ); + + await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + } + + public async Task> GetUserCommentsAsync(Guid userId) + { + var userCommentsDocument = await _collection.Find(uc => uc.UserId == userId).FirstOrDefaultAsync(); + return userCommentsDocument?.AsEntities() ?? Enumerable.Empty(); + } + + public async Task> GetUserCommentsPagedAsync(Guid userId, int pageNumber, int pageSize) + { + var userCommentsDocument = await _collection.Find(uc => uc.UserId == userId).FirstOrDefaultAsync(); + + if (userCommentsDocument == null) + { + return new PagedResponse(Enumerable.Empty(), pageNumber, pageSize, 0); + } + + var pagedComments = userCommentsDocument.Comments + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .Select(doc => doc.AsEntity()); + + return new PagedResponse(pagedComments, pageNumber, pageSize, userCommentsDocument.Comments.Count()); + } + + public async Task DeleteCommentAsync(Guid userId, Guid commentId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(uc => uc.UserId, userId), + Builders.Filter.ElemMatch(uc => uc.Comments, c => c.Id == commentId) + ); + + var update = Builders.Update.PullFilter(uc => uc.Comments, c => c.Id == commentId); + await _collection.UpdateOneAsync(filter, update); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/UserReactionsHistoryRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/UserReactionsHistoryRepository.cs new file mode 100644 index 000000000..c2174362e --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/UserReactionsHistoryRepository.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MongoDB.Driver; +using MiniSpace.Services.Posts.Core.Entities; +using MiniSpace.Services.Posts.Core.Repositories; +using MiniSpace.Services.Posts.Core.Wrappers; +using MiniSpace.Services.Posts.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.Posts.Infrastructure.Mongo.Repositories +{ + public class UserReactionsHistoryRepository : IUserReactionsHistoryRepository + { + private readonly IMongoCollection _collection; + + public UserReactionsHistoryRepository(IMongoDatabase database) + { + _collection = database.GetCollection("user_reactions_history"); + } + + public async Task SaveReactionAsync(Guid userId, Reaction reaction) + { + var filter = Builders.Filter.Eq(ur => ur.UserId, userId); + + var reactionDocument = reaction.AsDocument(); + + var update = Builders.Update.Combine( + Builders.Update.Push(ur => ur.Reactions, reactionDocument), + Builders.Update.SetOnInsert(ur => ur.UserId, userId), + Builders.Update.SetOnInsert(ur => ur.Id, Guid.NewGuid()) + ); + + await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + } + + public async Task> GetUserReactionsAsync(Guid userId) + { + var userReactionsDocument = await _collection.Find(ur => ur.UserId == userId).FirstOrDefaultAsync(); + return userReactionsDocument?.AsEntities() ?? Enumerable.Empty(); + } + + public async Task> GetUserReactionsPagedAsync(Guid userId, int pageNumber, int pageSize) + { + var userReactionsDocument = await _collection.Find(ur => ur.UserId == userId).FirstOrDefaultAsync(); + + if (userReactionsDocument == null) + { + return new PagedResponse(Enumerable.Empty(), pageNumber, pageSize, 0); + } + + var pagedReactions = userReactionsDocument.Reactions + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .Select(doc => doc.AsEntity()); + + return new PagedResponse(pagedReactions, pageNumber, pageSize, userReactionsDocument.Reactions.Count()); + } + + public async Task DeleteReactionAsync(Guid userId, Guid reactionId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(ur => ur.UserId, userId), + Builders.Filter.ElemMatch(ur => ur.Reactions, r => r.Id == reactionId) + ); + + var update = Builders.Update.PullFilter(ur => ur.Reactions, r => r.Id == reactionId); + await _collection.UpdateOneAsync(filter, update); + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Clients/FriendsServiceClient.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Clients/FriendsServiceClient.cs new file mode 100644 index 000000000..2951f8610 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Clients/FriendsServiceClient.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Convey.HTTP; +using MiniSpace.Services.Posts.Application.Dto; +using MiniSpace.Services.Posts.Application.Services.Clients; + +namespace MiniSpace.Services.Posts.Infrastructure.Services.Clients +{ + [ExcludeFromCodeCoverage] + public class FriendsServiceClient : IFriendsServiceClient + { + private readonly IHttpClient _httpClient; + private readonly string _url; + + public FriendsServiceClient(IHttpClient httpClient, HttpClientOptions options) + { + _httpClient = httpClient; + _url = options.Services["friends"]; + } + + public Task> GetAsync(Guid userId) + => _httpClient.GetAsync>($"{_url}/friends/{userId}"); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Clients/StudentsServiceClient.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Clients/StudentsServiceClient.cs index 39963ce07..41530b2e4 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Clients/StudentsServiceClient.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Clients/StudentsServiceClient.cs @@ -22,5 +22,7 @@ public StudentsServiceClient(IHttpClient httpClient, HttpClientOptions options) public Task GetAsync(Guid id) => _httpClient.GetAsync($"{_url}/students/{id}/events"); + public Task GetStudentByIdAsync(Guid studentId) + => _httpClient.GetAsync($"{_url}/students/{studentId}"); } -} \ No newline at end of file +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/PostRecommendationService.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/PostRecommendationService.cs new file mode 100644 index 000000000..7e8a40158 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/PostRecommendationService.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.ML; +using Microsoft.ML.Data; +using MiniSpace.Services.Posts.Application.Dto; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Infrastructure.Services.Recommendation; + +namespace MiniSpace.Services.Posts.Infrastructure.Services +{ + public class PostRecommendationService : IPostRecommendationService + { + private readonly MLContext _mlContext; + + public PostRecommendationService(MLContext mlContext) + { + _mlContext = mlContext; + } + + public async Task> RankPostsByUserInterestAsync(Guid userId, IEnumerable posts, IDictionary userInterests) + { + // Step 1: Prepare the training data + var trainingData = PrepareTrainingData(userInterests, posts); + + // Step 2: Train the model + var model = TrainModel(trainingData); + + // Step 3: Use the trained model to rank posts + var rankedPosts = new List<(PostDto Post, double Score)>(); + var predictionEngine = _mlContext.Model.CreatePredictionEngine(model); + + foreach (var post in posts) + { + var input = new PostInputModel + { + TextContent = post.TextContent, + TextLength = post.TextContent.Length, + KeywordMatchCount = (float)userInterests.Where(ui => post.TextContent.Contains(ui.Key)).Sum(ui => ui.Value), + PostAgeDays = (post.PublishDate.HasValue ? (float)(DateTime.UtcNow - post.PublishDate.Value).TotalDays : 0) + }; + + var prediction = predictionEngine.Predict(input); + rankedPosts.Add((post, prediction.Score)); + } + + return rankedPosts.OrderByDescending(x => x.Score); + } + + private IDataView PrepareTrainingData(IDictionary userInterests, IEnumerable posts) + { + var inputData = posts.Select(post => + { + var keywordMatches = userInterests.Where(ui => post.TextContent.Contains(ui.Key)).Sum(ui => ui.Value); + return new PostInputModel + { + TextContent = post.TextContent, + Label = (float)keywordMatches, + TextLength = post.TextContent.Length, + KeywordMatchCount = (float)keywordMatches, + PostAgeDays = (post.PublishDate.HasValue ? (float)(DateTime.UtcNow - post.PublishDate.Value).TotalDays : 0) + }; + }); + + return _mlContext.Data.LoadFromEnumerable(inputData); + } + + private ITransformer TrainModel(IDataView trainingData) + { + var dataProcessPipeline = _mlContext.Transforms.Text.FeaturizeText("Features", nameof(PostInputModel.TextContent)); + + var trainer = _mlContext.Regression.Trainers.Sdca(labelColumnName: "Label", featureColumnName: "Features"); + + var trainingPipeline = dataProcessPipeline.Append(trainer); + + var model = trainingPipeline.Fit(trainingData); + return model; + } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Recommendation/PostInputModel.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Recommendation/PostInputModel.cs new file mode 100644 index 000000000..3718ccab9 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Recommendation/PostInputModel.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using Microsoft.ML.Data; + +namespace MiniSpace.Services.Posts.Infrastructure.Services.Recommendation +{ + public class PostInputModel + { + [LoadColumn(0)] + public string TextContent { get; set; } + + [LoadColumn(1)] + public float Label { get; set; } + + [LoadColumn(2)] + public float TextLength { get; set; } + + [LoadColumn(3)] + public float KeywordMatchCount { get; set; } + + [LoadColumn(4)] + public float PostAgeDays { get; set; } + } +} diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Recommendation/PostPrediction.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Recommendation/PostPrediction.cs new file mode 100644 index 000000000..529bd630c --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Services/Recommendation/PostPrediction.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.ML.Data; + +namespace MiniSpace.Services.Posts.Infrastructure.Services.Recommendation +{ + public class PostPrediction + { + [ColumnName("Score")] + public float Score { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/Program.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/Program.cs index a1961adcf..2d0f580b2 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/Program.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Api/Program.cs @@ -37,6 +37,8 @@ public static async Task Main(string[] args) .Get("reactions/summary") .Post("reactions", afterDispatch: (cmd, ctx) => ctx.Response.Created($"reactions/{cmd.ReactionId}")) + .Put("reactions/{reactionId}", + afterDispatch: (cmd, ctx) => ctx.Response.NoContent()) .Delete("reactions/{reactionId}") )) .UseLogging() diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/Handlers/CreateReactionHandler.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/Handlers/CreateReactionHandler.cs index de5991a05..9f39e64c0 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/Handlers/CreateReactionHandler.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/Handlers/CreateReactionHandler.cs @@ -68,21 +68,81 @@ public async Task HandleAsync(CreateReaction command, CancellationToken cancella var targetType = ParseTargetType(command.TargetType); var reactionType = ParseReactionType(command.ReactionType); - await EnsureReactionDoesNotExistAsync(command.ContentId, contentType, command.UserId); + var existingReaction = await FindExistingReactionAsync(command.ContentId, command.UserId, contentType, targetType); - // Check if the comment exists using the CommentServiceClient - if (contentType == ReactionContentType.Comment) + if (existingReaction != null) { - if (!await _commentServiceClient.CommentExistsAsync(command.ContentId)) - { - throw new CommentNotFoundException(command.ContentId); - } + // Update the existing reaction + existingReaction.UpdateReactionType(reactionType); + await UpdateReactionAsync(existingReaction, contentType, targetType); + } + else + { + // Create a new reaction + var reaction = Reaction.Create(command.ReactionId, command.UserId, reactionType, + command.ContentId, contentType, targetType); + + // Add the reaction to the appropriate repository + await AddReactionAsync(reaction, contentType, targetType); + } + + var reactionCreatedEvent = new ReactionCreated( + command.ReactionId, + command.UserId, + command.ContentId, + command.ContentType, + command.ReactionType, + command.TargetType + ); + + await _messageBroker.PublishAsync(reactionCreatedEvent); + } + + private async Task FindExistingReactionAsync(Guid contentId, Guid userId, ReactionContentType contentType, ReactionTargetType targetType) + { + // Check in each repository if a reaction already exists for the user and content + switch (contentType) + { + case ReactionContentType.Event when targetType == ReactionTargetType.Organization: + return await _orgEventRepository.GetAsync(contentId, userId); + + case ReactionContentType.Event when targetType == ReactionTargetType.User: + return await _userEventRepository.GetAsync(contentId, userId); + + case ReactionContentType.Post when targetType == ReactionTargetType.Organization: + return await _orgPostRepository.GetAsync(contentId, userId); + + case ReactionContentType.Post when targetType == ReactionTargetType.User: + return await _userPostRepository.GetAsync(contentId, userId); + + case ReactionContentType.Comment when targetType == ReactionTargetType.Organization: + if (await _orgEventCommentsRepository.ExistsAsync(contentId)) + { + return await _orgEventCommentsRepository.GetAsync(contentId, userId); + } + else if (await _orgPostCommentsRepository.ExistsAsync(contentId)) + { + return await _orgPostCommentsRepository.GetAsync(contentId, userId); + } + break; + + case ReactionContentType.Comment when targetType == ReactionTargetType.User: + if (await _userEventCommentsRepository.ExistsAsync(contentId)) + { + return await _userEventCommentsRepository.GetAsync(contentId, userId); + } + else if (await _userPostCommentsRepository.ExistsAsync(contentId)) + { + return await _userPostCommentsRepository.GetAsync(contentId, userId); + } + break; } - var reaction = Reaction.Create(command.ReactionId, command.UserId, reactionType, - command.ContentId, contentType, targetType); + return null; + } - // Add the reaction to the appropriate repository based on content and target types + private async Task AddReactionAsync(Reaction reaction, ReactionContentType contentType, ReactionTargetType targetType) + { switch (contentType) { case ReactionContentType.Event when targetType == ReactionTargetType.Organization: @@ -102,32 +162,77 @@ public async Task HandleAsync(CreateReaction command, CancellationToken cancella break; case ReactionContentType.Comment when targetType == ReactionTargetType.Organization: - if (await _orgEventCommentsRepository.ExistsAsync(command.ContentId)) + if (await _orgEventCommentsRepository.ExistsAsync(reaction.ContentId)) { await _orgEventCommentsRepository.AddAsync(reaction); } - else if (await _orgPostCommentsRepository.ExistsAsync(command.ContentId)) + else if (await _orgPostCommentsRepository.ExistsAsync(reaction.ContentId)) { await _orgPostCommentsRepository.AddAsync(reaction); } break; case ReactionContentType.Comment when targetType == ReactionTargetType.User: - if (await _userEventCommentsRepository.ExistsAsync(command.ContentId)) + if (await _userEventCommentsRepository.ExistsAsync(reaction.ContentId)) { await _userEventCommentsRepository.AddAsync(reaction); } - else if (await _userPostCommentsRepository.ExistsAsync(command.ContentId)) + else if (await _userPostCommentsRepository.ExistsAsync(reaction.ContentId)) { await _userPostCommentsRepository.AddAsync(reaction); } break; default: - throw new InvalidReactionContentTypeException(command.ContentType); + throw new InvalidReactionContentTypeException(reaction.ContentType.ToString()); } + } - await _messageBroker.PublishAsync(new ReactionCreated(command.ReactionId)); + private async Task UpdateReactionAsync(Reaction reaction, ReactionContentType contentType, ReactionTargetType targetType) + { + switch (contentType) + { + case ReactionContentType.Event when targetType == ReactionTargetType.Organization: + await _orgEventRepository.UpdateAsync(reaction); + break; + + case ReactionContentType.Event when targetType == ReactionTargetType.User: + await _userEventRepository.UpdateAsync(reaction); + break; + + case ReactionContentType.Post when targetType == ReactionTargetType.Organization: + await _orgPostRepository.UpdateAsync(reaction); + break; + + case ReactionContentType.Post when targetType == ReactionTargetType.User: + await _userPostRepository.UpdateAsync(reaction); + break; + + case ReactionContentType.Comment when targetType == ReactionTargetType.Organization: + if (await _orgEventCommentsRepository.ExistsAsync(reaction.ContentId)) + { + await _orgEventCommentsRepository.UpdateAsync(reaction); + } + else if (await _orgPostCommentsRepository.ExistsAsync(reaction.ContentId)) + { + await _orgPostCommentsRepository.UpdateAsync(reaction); + } + break; + + case ReactionContentType.Comment when targetType == ReactionTargetType.User: + if (await _userEventCommentsRepository.ExistsAsync(reaction.ContentId)) + { + await _userEventCommentsRepository.UpdateAsync(reaction); + } + else if (await _userPostCommentsRepository.ExistsAsync(reaction.ContentId)) + { + await _userPostCommentsRepository.UpdateAsync(reaction); + } + break; + + default: + throw new InvalidReactionContentTypeException(reaction.ContentType.ToString()); + } } private void ValidateStudentIdentity(CreateReaction command) @@ -177,13 +282,5 @@ private ReactionType ParseReactionType(string reactionType) return parsedReactionType; } - - private async Task EnsureReactionDoesNotExistAsync(Guid contentId, ReactionContentType contentType, Guid userId) - { - if (await _reactionRepository.ExistsAsync(contentId, contentType, userId)) - { - throw new StudentAlreadyGaveReactionException(userId, contentId, contentType); - } - } } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/Handlers/UpdateReactionHandler.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/Handlers/UpdateReactionHandler.cs new file mode 100644 index 000000000..2abec7466 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/Handlers/UpdateReactionHandler.cs @@ -0,0 +1,170 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Reactions.Application.Exceptions; +using MiniSpace.Services.Reactions.Core.Entities; +using MiniSpace.Services.Reactions.Core.Repositories; +using System; +using System.Threading; +using System.Threading.Tasks; +using MiniSpace.Services.Reactions.Core.Exceptions; + +namespace MiniSpace.Services.Reactions.Application.Commands.Handlers +{ + public class UpdateReactionHandler : ICommandHandler + { + private readonly IReactionsOrganizationsEventRepository _orgEventRepository; + private readonly IReactionsOrganizationsPostRepository _orgPostRepository; + private readonly IReactionsUserEventRepository _userEventRepository; + private readonly IReactionsUserPostRepository _userPostRepository; + private readonly IReactionsOrganizationsEventCommentsRepository _orgEventCommentsRepository; + private readonly IReactionsOrganizationsPostCommentsRepository _orgPostCommentsRepository; + private readonly IReactionsUserEventCommentsRepository _userEventCommentsRepository; + private readonly IReactionsUserPostCommentsRepository _userPostCommentsRepository; + private readonly IAppContext _appContext; + + public UpdateReactionHandler( + IReactionsOrganizationsEventRepository orgEventRepository, + IReactionsOrganizationsPostRepository orgPostRepository, + IReactionsUserEventRepository userEventRepository, + IReactionsUserPostRepository userPostRepository, + IReactionsOrganizationsEventCommentsRepository orgEventCommentsRepository, + IReactionsOrganizationsPostCommentsRepository orgPostCommentsRepository, + IReactionsUserEventCommentsRepository userEventCommentsRepository, + IReactionsUserPostCommentsRepository userPostCommentsRepository, + IAppContext appContext) + { + _orgEventRepository = orgEventRepository; + _orgPostRepository = orgPostRepository; + _userEventRepository = userEventRepository; + _userPostRepository = userPostRepository; + _orgEventCommentsRepository = orgEventCommentsRepository; + _orgPostCommentsRepository = orgPostCommentsRepository; + _userEventCommentsRepository = userEventCommentsRepository; + _userPostCommentsRepository = userPostCommentsRepository; + _appContext = appContext; + } + + public async Task HandleAsync(UpdateReaction command, CancellationToken cancellationToken = default) + { + ValidateUserIdentity(command); + + var contentType = ParseContentType(command.ContentType); + var targetType = ParseTargetType(command.TargetType); + + // Attempt to find the reaction in the appropriate repository using the ReactionId + var reaction = await FindExistingReactionAsync(command.ReactionId, contentType, targetType); + if (reaction == null) + { + throw new ReactionNotFoundException(command.ReactionId); + } + + // Ensure the reaction belongs to the correct user + if (reaction.UserId != command.UserId) + { + throw new UnauthorizedReactionAccessException(command.ReactionId, command.UserId); + } + + // Update the reaction type + var newReactionType = ParseReactionType(command.NewReactionType); + reaction.UpdateReactionType(newReactionType); + + // Update the reaction in the appropriate repository + await UpdateReactionAsync(reaction, contentType, targetType); + } + + private async Task FindExistingReactionAsync(Guid reactionId, ReactionContentType contentType, ReactionTargetType targetType) + { + // Retrieve the reaction from the correct repository based on content and target type + return contentType switch + { + ReactionContentType.Event when targetType == ReactionTargetType.Organization => await _orgEventRepository.GetByIdAsync(reactionId), + ReactionContentType.Event when targetType == ReactionTargetType.User => await _userEventRepository.GetByIdAsync(reactionId), + ReactionContentType.Post when targetType == ReactionTargetType.Organization => await _orgPostRepository.GetByIdAsync(reactionId), + ReactionContentType.Post when targetType == ReactionTargetType.User => await _userPostRepository.GetByIdAsync(reactionId), + ReactionContentType.Comment when targetType == ReactionTargetType.Organization => await _orgEventCommentsRepository.GetByIdAsync(reactionId) ?? await _orgPostCommentsRepository.GetByIdAsync(reactionId), + ReactionContentType.Comment when targetType == ReactionTargetType.User => await _userEventCommentsRepository.GetByIdAsync(reactionId) ?? await _userPostCommentsRepository.GetByIdAsync(reactionId), + _ => null, + }; + } + + private async Task UpdateReactionAsync(Reaction reaction, ReactionContentType contentType, ReactionTargetType targetType) + { + switch (contentType) + { + case ReactionContentType.Event when targetType == ReactionTargetType.Organization: + await _orgEventRepository.UpdateAsync(reaction); + break; + case ReactionContentType.Event when targetType == ReactionTargetType.User: + await _userEventRepository.UpdateAsync(reaction); + break; + case ReactionContentType.Post when targetType == ReactionTargetType.Organization: + await _orgPostRepository.UpdateAsync(reaction); + break; + case ReactionContentType.Post when targetType == ReactionTargetType.User: + await _userPostRepository.UpdateAsync(reaction); + break; + case ReactionContentType.Comment when targetType == ReactionTargetType.Organization: + if (await _orgEventCommentsRepository.ExistsAsync(reaction.ContentId)) + { + await _orgEventCommentsRepository.UpdateAsync(reaction); + } + else if (await _orgPostCommentsRepository.ExistsAsync(reaction.ContentId)) + { + await _orgPostCommentsRepository.UpdateAsync(reaction); + } + break; + case ReactionContentType.Comment when targetType == ReactionTargetType.User: + if (await _userEventCommentsRepository.ExistsAsync(reaction.ContentId)) + { + await _userEventCommentsRepository.UpdateAsync(reaction); + } + else if (await _userPostCommentsRepository.ExistsAsync(reaction.ContentId)) + { + await _userPostCommentsRepository.UpdateAsync(reaction); + } + break; + default: + throw new InvalidReactionContentTypeException(contentType.ToString()); + } + } + + private void ValidateUserIdentity(UpdateReaction command) + { + var identity = _appContext.Identity; + + if (identity.IsAuthenticated && identity.Id != command.UserId) + { + throw new UnauthorizedReactionAccessException(command.ReactionId, command.UserId); + } + } + + private ReactionContentType ParseContentType(string contentType) + { + if (!Enum.TryParse(contentType, true, out var parsedContentType)) + { + throw new InvalidReactionContentTypeException(contentType); + } + + return parsedContentType; + } + + private ReactionTargetType ParseTargetType(string targetType) + { + if (!Enum.TryParse(targetType, true, out var parsedTargetType)) + { + throw new InvalidReactionTargetTypeException(targetType); + } + + return parsedTargetType; + } + + private ReactionType ParseReactionType(string reactionType) + { + if (!Enum.TryParse(reactionType, true, out var parsedReactionType)) + { + throw new InvalidReactionTypeException(reactionType); + } + + return parsedReactionType; + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/UpdateReaction.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/UpdateReaction.cs new file mode 100644 index 000000000..985807695 --- /dev/null +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Commands/UpdateReaction.cs @@ -0,0 +1,23 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Reactions.Application.Commands +{ + public class UpdateReaction : ICommand + { + public Guid ReactionId { get; } + public Guid UserId { get; } + public string NewReactionType { get; } + public string ContentType { get; } + public string TargetType { get; } + + public UpdateReaction(Guid reactionId, Guid userId, string newReactionType, string contentType, string targetType) + { + ReactionId = reactionId; + UserId = userId; + NewReactionType = newReactionType; + ContentType = contentType; + TargetType = targetType; + } + } +} diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Dto/ReactionsSummaryDto.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Dto/ReactionsSummaryDto.cs index 76ba0bbf1..d8e2cc3dc 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Dto/ReactionsSummaryDto.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Dto/ReactionsSummaryDto.cs @@ -11,5 +11,6 @@ public class ReactionsSummaryDto(int numerOfReactions, ReactionType? dominant, G public ReactionType? DominantReaction { get; set; } = dominant; public Guid? AuthUserReactionId { get; set; } = authUserReactionId; public ReactionType? AuthUserReactionType { get; set; } = authUserReactionType; + public Dictionary ReactionsWithCounts { get; set; } = new Dictionary(); } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/ReactionCreated.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/ReactionCreated.cs index c3b5031a9..facdcb81c 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/ReactionCreated.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Application/Events/ReactionCreated.cs @@ -1,16 +1,25 @@ -using System.Net.Mime; using Convey.CQRS.Events; -using Microsoft.AspNetCore.Connections; -using MiniSpace.Services.Reactions.Core.Entities; +using System; namespace MiniSpace.Services.Reactions.Application.Events { public class ReactionCreated : IEvent { - public Guid ReactionId {get;set;} - public ReactionCreated(Guid reactionId) + public Guid ReactionId { get; } + public Guid UserId { get; } + public Guid ContentId { get; } + public string ContentType { get; } + public string ReactionType { get; } + public string TargetType { get; } + + public ReactionCreated(Guid reactionId, Guid userId, Guid contentId, string contentType, string reactionType, string targetType) { - ReactionId=reactionId; + ReactionId = reactionId; + UserId = userId; + ContentId = contentId; + ContentType = contentType; + ReactionType = reactionType; + TargetType = targetType; } } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionRepository.cs index 6c995d83a..0bbed931d 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionRepository.cs @@ -10,6 +10,7 @@ public interface IReactionRepository Task AddAsync(Reaction reaction); Task DeleteAsync(Guid id); Task ExistsAsync(Guid contentId, ReactionContentType contentType, Guid studentId); + Task GetByIdAsync(Guid id, Guid userId); } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsEventCommentsRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsEventCommentsRepository.cs index 064e6c05b..4aba43906 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsEventCommentsRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsEventCommentsRepository.cs @@ -11,5 +11,7 @@ public interface IReactionsOrganizationsEventCommentsRepository Task AddAsync(Reaction reaction); Task UpdateAsync(Reaction reaction); Task DeleteAsync(Guid id); + Task> GetByContentIdAsync(Guid contentId); + Task GetAsync(Guid contentId, Guid userId); } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsEventRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsEventRepository.cs index c0fcb095e..521f761bc 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsEventRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsEventRepository.cs @@ -11,5 +11,7 @@ public interface IReactionsOrganizationsEventRepository Task AddAsync(Reaction reaction); Task UpdateAsync(Reaction reaction); Task DeleteAsync(Guid id); + Task> GetByContentIdAsync(Guid contentId); + Task GetAsync(Guid contentId, Guid userId); } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsPostCommentsRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsPostCommentsRepository.cs index f07356bba..83b1c56be 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsPostCommentsRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsPostCommentsRepository.cs @@ -11,5 +11,7 @@ public interface IReactionsOrganizationsPostCommentsRepository Task AddAsync(Reaction reaction); Task UpdateAsync(Reaction reaction); Task DeleteAsync(Guid id); + Task> GetByContentIdAsync(Guid contentId); + Task GetAsync(Guid contentId, Guid userId); } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsPostRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsPostRepository.cs index 49d53c68a..a581670fb 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsPostRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsOrganizationsPostRepository.cs @@ -11,5 +11,7 @@ public interface IReactionsOrganizationsPostRepository Task AddAsync(Reaction reaction); Task UpdateAsync(Reaction reaction); Task DeleteAsync(Guid id); + Task> GetByContentIdAsync(Guid contentId); + Task GetAsync(Guid contentId, Guid userId); } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserEventCommentsRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserEventCommentsRepository.cs index 45462e143..f4d68bc21 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserEventCommentsRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserEventCommentsRepository.cs @@ -11,5 +11,7 @@ public interface IReactionsUserEventCommentsRepository Task AddAsync(Reaction reaction); Task UpdateAsync(Reaction reaction); Task DeleteAsync(Guid id); + Task> GetByContentIdAsync(Guid contentId); + Task GetAsync(Guid contentId, Guid userId); } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserEventRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserEventRepository.cs index bfe750bc7..acc9a4cfa 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserEventRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserEventRepository.cs @@ -11,5 +11,7 @@ public interface IReactionsUserEventRepository Task AddAsync(Reaction reaction); Task UpdateAsync(Reaction reaction); Task DeleteAsync(Guid id); + Task> GetByContentIdAsync(Guid contentId); + Task GetAsync(Guid contentId, Guid userId); } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserPostCommentsRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserPostCommentsRepository.cs index 5426156c5..559697bbe 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserPostCommentsRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserPostCommentsRepository.cs @@ -11,5 +11,7 @@ public interface IReactionsUserPostCommentsRepository Task AddAsync(Reaction reaction); Task UpdateAsync(Reaction reaction); Task DeleteAsync(Guid id); + Task> GetByContentIdAsync(Guid contentId); + Task GetAsync(Guid contentId, Guid userId); } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserPostRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserPostRepository.cs index 3440bc61b..8dbc36b22 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserPostRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Core/Repositories/IReactionsUserPostRepository.cs @@ -10,6 +10,8 @@ public interface IReactionsUserPostRepository Task GetByIdAsync(Guid id); Task AddAsync(Reaction reaction); Task UpdateAsync(Reaction reaction); - Task DeleteAsync(Guid id); + Task DeleteAsync(Guid id); + Task> GetByContentIdAsync(Guid contentId); + Task GetAsync(Guid contentId, Guid userId); } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Extensions.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Extensions.cs index 99fe5159d..296a30ac0 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Extensions.cs @@ -42,6 +42,8 @@ using MiniSpace.Services.Reactions.Application.Events; using System.Diagnostics.CodeAnalysis; +using MiniSpace.Services.Reactions.Application.Services.Clients; +using MiniSpace.Services.Reactions.Infrastructure.Services.Clients; namespace MiniSpace.Services.Reactions.Infrastructure { @@ -61,6 +63,9 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddTransient(); diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/Extensions.cs index 160097a89..61ae50dfd 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/Extensions.cs @@ -175,6 +175,32 @@ public static IEnumerable ToEntities(this UserPostCommentsReactionDocu return document.Reactions.Select(r => r.ToEntity()); } + public static ReactionDto AsDto(this Reaction reaction) + { + return new ReactionDto + { + Id = reaction.Id, + UserId = reaction.UserId, + ContentId = reaction.ContentId, + ContentType = reaction.ContentType, + ReactionType = reaction.ReactionType, + TargetType = reaction.TargetType + }; + } + + // public static ReactionDto AsDto(this ReactionDocument document) + // { + // return new ReactionDto + // { + // Id = document.Id, + // UserId = document.UserId, + // ContentId = document.ContentId, + // ContentType = document.ContentType, + // ReactionType = document.ReactionType, + // TargetType = document.TargetType + // }; + // } + public static UpdateDefinition Push( this UpdateDefinitionBuilder builder, diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/ReactionDocument.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/ReactionDocument.cs index 8edffceb9..3508b1563 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/ReactionDocument.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Documents/ReactionDocument.cs @@ -1,6 +1,8 @@ using System; using Convey.Types; using MiniSpace.Services.Reactions.Core.Entities; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; namespace MiniSpace.Services.Reactions.Infrastructure.Mongo.Documents { diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Queries/Handlers/GetReactionsHandler.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Queries/Handlers/GetReactionsHandler.cs index f94777694..7792cbbdd 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Queries/Handlers/GetReactionsHandler.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Queries/Handlers/GetReactionsHandler.cs @@ -1,34 +1,83 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Convey.CQRS.Queries; -using Convey.Persistence.MongoDB; -using DnsClient; using MiniSpace.Services.Reactions.Application.Dto; using MiniSpace.Services.Reactions.Application.Queries; using MiniSpace.Services.Reactions.Core.Entities; -using MiniSpace.Services.Reactions.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Reactions.Core.Repositories; using MiniSpace.Services.Reactions.Infrastructure.Mongo.Extensions; -using MongoDB.Driver; -using MongoDB.Driver.Linq; namespace MiniSpace.Services.Reactions.Infrastructure.Mongo.Queries.Handlers { - [ExcludeFromCodeCoverage] public class GetReactionsHandler : IQueryHandler> { - private readonly IMongoRepository _reactionRepository; + private readonly IReactionsUserPostRepository _userPostRepository; + private readonly IReactionsOrganizationsPostRepository _orgPostRepository; + private readonly IReactionsUserEventRepository _userEventRepository; + private readonly IReactionsOrganizationsEventRepository _orgEventRepository; + private readonly IReactionsUserPostCommentsRepository _userPostCommentsRepository; + private readonly IReactionsOrganizationsPostCommentsRepository _orgPostCommentsRepository; + private readonly IReactionsUserEventCommentsRepository _userEventCommentsRepository; + private readonly IReactionsOrganizationsEventCommentsRepository _orgEventCommentsRepository; - public GetReactionsHandler(IMongoRepository reactionRepository) + public GetReactionsHandler( + IReactionsUserPostRepository userPostRepository, + IReactionsOrganizationsPostRepository orgPostRepository, + IReactionsUserEventRepository userEventRepository, + IReactionsOrganizationsEventRepository orgEventRepository, + IReactionsUserPostCommentsRepository userPostCommentsRepository, + IReactionsOrganizationsPostCommentsRepository orgPostCommentsRepository, + IReactionsUserEventCommentsRepository userEventCommentsRepository, + IReactionsOrganizationsEventCommentsRepository orgEventCommentsRepository) { - _reactionRepository = reactionRepository; + _userPostRepository = userPostRepository; + _orgPostRepository = orgPostRepository; + _userEventRepository = userEventRepository; + _orgEventRepository = orgEventRepository; + _userPostCommentsRepository = userPostCommentsRepository; + _orgPostCommentsRepository = orgPostCommentsRepository; + _userEventCommentsRepository = userEventCommentsRepository; + _orgEventCommentsRepository = orgEventCommentsRepository; } - + public async Task> HandleAsync(GetReactions query, CancellationToken cancellationToken) { - var documents = _reactionRepository.Collection.AsQueryable(); - documents = documents.Where(p => p.ContentId == query.ContentId && p.ContentType == query.ContentType); + List reactions = new(); + + switch (query.ContentType) + { + case ReactionContentType.Post: + var userPostReactions = await _userPostRepository.GetByContentIdAsync(query.ContentId); + var orgPostReactions = await _orgPostRepository.GetByContentIdAsync(query.ContentId); + + reactions.AddRange(userPostReactions); + reactions.AddRange(orgPostReactions); + break; + + case ReactionContentType.Event: + var userEventReactions = await _userEventRepository.GetByContentIdAsync(query.ContentId); + var orgEventReactions = await _orgEventRepository.GetByContentIdAsync(query.ContentId); + + reactions.AddRange(userEventReactions); + reactions.AddRange(orgEventReactions); + break; + + case ReactionContentType.Comment: + var userPostCommentReactions = await _userPostCommentsRepository.GetByContentIdAsync(query.ContentId); + var orgPostCommentReactions = await _orgPostCommentsRepository.GetByContentIdAsync(query.ContentId); + var userEventCommentReactions = await _userEventCommentsRepository.GetByContentIdAsync(query.ContentId); + var orgEventCommentReactions = await _orgEventCommentsRepository.GetByContentIdAsync(query.ContentId); + + reactions.AddRange(userPostCommentReactions); + reactions.AddRange(orgPostCommentReactions); + reactions.AddRange(userEventCommentReactions); + reactions.AddRange(orgEventCommentReactions); + break; + } - var reactions = await documents.ToListAsync(); - return reactions.Select(p => p.AsDto()); + return reactions.Select(r => r.AsDto()); } - } + } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Queries/Handlers/GetReactionsSummaryHandler.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Queries/Handlers/GetReactionsSummaryHandler.cs index 52af13fcf..652eb63d5 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Queries/Handlers/GetReactionsSummaryHandler.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Queries/Handlers/GetReactionsSummaryHandler.cs @@ -1,39 +1,89 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Convey.CQRS.Queries; -using Convey.Persistence.MongoDB; using MiniSpace.Services.Reactions.Application; using MiniSpace.Services.Reactions.Application.Dto; using MiniSpace.Services.Reactions.Application.Queries; using MiniSpace.Services.Reactions.Core.Entities; -using MiniSpace.Services.Reactions.Infrastructure.Mongo.Documents; -using MongoDB.Driver; -using MongoDB.Driver.Linq; +using MiniSpace.Services.Reactions.Core.Repositories; namespace MiniSpace.Services.Reactions.Infrastructure.Mongo.Queries.Handlers { - [ExcludeFromCodeCoverage] public class GetReactionsSummaryHandler : IQueryHandler { - private readonly IMongoRepository _reactionRepository; - + private readonly IReactionsUserPostRepository _userPostRepository; + private readonly IReactionsOrganizationsPostRepository _orgPostRepository; + private readonly IReactionsUserEventRepository _userEventRepository; + private readonly IReactionsOrganizationsEventRepository _orgEventRepository; + private readonly IReactionsUserPostCommentsRepository _userPostCommentsRepository; + private readonly IReactionsOrganizationsPostCommentsRepository _orgPostCommentsRepository; + private readonly IReactionsUserEventCommentsRepository _userEventCommentsRepository; + private readonly IReactionsOrganizationsEventCommentsRepository _orgEventCommentsRepository; private readonly IAppContext _appContext; - public GetReactionsSummaryHandler(IMongoRepository reactionRepository, IAppContext appContext) + public GetReactionsSummaryHandler( + IReactionsUserPostRepository userPostRepository, + IReactionsOrganizationsPostRepository orgPostRepository, + IReactionsUserEventRepository userEventRepository, + IReactionsOrganizationsEventRepository orgEventRepository, + IReactionsUserPostCommentsRepository userPostCommentsRepository, + IReactionsOrganizationsPostCommentsRepository orgPostCommentsRepository, + IReactionsUserEventCommentsRepository userEventCommentsRepository, + IReactionsOrganizationsEventCommentsRepository orgEventCommentsRepository, + IAppContext appContext) { - _reactionRepository = reactionRepository; + _userPostRepository = userPostRepository; + _orgPostRepository = orgPostRepository; + _userEventRepository = userEventRepository; + _orgEventRepository = orgEventRepository; + _userPostCommentsRepository = userPostCommentsRepository; + _orgPostCommentsRepository = orgPostCommentsRepository; + _userEventCommentsRepository = userEventCommentsRepository; + _orgEventCommentsRepository = orgEventCommentsRepository; _appContext = appContext; } - - public async Task - HandleAsync(GetReactionsSummary query, CancellationToken cancellationToken) + + public async Task HandleAsync(GetReactionsSummary query, CancellationToken cancellationToken) { - var documents = _reactionRepository.Collection.AsQueryable(); - documents = documents.Where(p => p.ContentId == query.ContentId && p.ContentType == query.ContentType); + List reactions = new(); + + switch (query.ContentType) + { + case ReactionContentType.Post: + var userPostReactions = await _userPostRepository.GetByContentIdAsync(query.ContentId); + var orgPostReactions = await _orgPostRepository.GetByContentIdAsync(query.ContentId); + + reactions.AddRange(userPostReactions); + reactions.AddRange(orgPostReactions); + break; + + case ReactionContentType.Event: + var userEventReactions = await _userEventRepository.GetByContentIdAsync(query.ContentId); + var orgEventReactions = await _orgEventRepository.GetByContentIdAsync(query.ContentId); + + reactions.AddRange(userEventReactions); + reactions.AddRange(orgEventReactions); + break; + + case ReactionContentType.Comment: + var userPostCommentReactions = await _userPostCommentsRepository.GetByContentIdAsync(query.ContentId); + var orgPostCommentReactions = await _orgPostCommentsRepository.GetByContentIdAsync(query.ContentId); + var userEventCommentReactions = await _userEventCommentsRepository.GetByContentIdAsync(query.ContentId); + var orgEventCommentReactions = await _orgEventCommentsRepository.GetByContentIdAsync(query.ContentId); + + reactions.AddRange(userPostCommentReactions); + reactions.AddRange(orgPostCommentReactions); + reactions.AddRange(userEventCommentReactions); + reactions.AddRange(orgEventCommentReactions); + break; + } - var reactions = await documents.ToListAsync(); int nrReactions = reactions.Count; - if (nrReactions == 0) { + if (nrReactions == 0) + { return new ReactionsSummaryDto(0, null, null, null); } @@ -41,15 +91,29 @@ public async Task Guid? authUserReactionId = null; ReactionType? authUserReactionType = null; - if (identity.IsAuthenticated && reactions.Exists(x => x.UserId == identity.Id)) { - var reactionDocument = reactions.Find(x => x.UserId == identity.Id); - authUserReactionId = reactionDocument.Id; - authUserReactionType = reactionDocument.ReactionType; + if (identity.IsAuthenticated) + { + var userReaction = reactions.FirstOrDefault(r => r.UserId == identity.Id); + if (userReaction != null) + { + authUserReactionId = userReaction.Id; + authUserReactionType = userReaction.ReactionType; + } } - ReactionType dominant = reactions.GroupBy(x => x.ReactionType) - .OrderBy(x => x.ToList().Count).Last().Key; - return new ReactionsSummaryDto(nrReactions, dominant, authUserReactionId, authUserReactionType); + var dominantReaction = reactions + .GroupBy(r => r.ReactionType) + .OrderByDescending(g => g.Count()) + .First().Key; + + var reactionsWithCounts = reactions + .GroupBy(r => r.ReactionType) + .ToDictionary(g => g.Key, g => g.Count()); + + return new ReactionsSummaryDto(nrReactions, dominantReaction, authUserReactionId, authUserReactionType) + { + ReactionsWithCounts = reactionsWithCounts + }; } - } + } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionMongoRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionMongoRepository.cs index 2de1d1a73..518900901 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionMongoRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionMongoRepository.cs @@ -42,5 +42,11 @@ public Task DeleteAsync(Guid id) public Task ExistsAsync(Guid contentId, ReactionContentType contentType, Guid studentId) => _repository.ExistsAsync(x => x.ContentId == contentId && x.ContentType == contentType && x.UserId == studentId); + + public async Task GetByIdAsync(Guid id, Guid userId) + { + var reaction = await _repository.GetAsync(x => x.Id == id && x.UserId == userId); + return reaction?.AsEntity(); + } } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsEventCommentsMongoRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsEventCommentsMongoRepository.cs index 961b766ac..94373d7f4 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsEventCommentsMongoRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsEventCommentsMongoRepository.cs @@ -29,12 +29,22 @@ public async Task GetByIdAsync(Guid id) public async Task AddAsync(Reaction reaction) { - var filter = Builders.Filter.Eq(x => x.OrganizationEventCommentId, reaction.ContentId); - var update = Builders.Update.Push(x => x.Reactions, reaction.AsDocument()); + var filter = Builders.Filter.Eq(d => d.OrganizationEventCommentId, reaction.ContentId); - await _repository.Collection.UpdateOneAsync(filter, update); - } + var update = Builders.Update.Combine( + Builders.Update.Push(d => d.Reactions, reaction.AsDocument()), + Builders.Update.SetOnInsert(d => d.OrganizationEventCommentId, reaction.ContentId), + Builders.Update.SetOnInsert(d => d.Id, Guid.NewGuid()) + ); + + var options = new UpdateOptions { IsUpsert = true }; + var result = await _repository.Collection.UpdateOneAsync(filter, update, options); + if (!result.IsAcknowledged || result.ModifiedCount == 0) + { + } + } + public async Task UpdateAsync(Reaction reaction) { var filter = Builders.Filter.And( @@ -54,5 +64,23 @@ public async Task DeleteAsync(Guid id) await _repository.Collection.UpdateOneAsync(filter, update); } + + public async Task> GetByContentIdAsync(Guid contentId) + { + var document = await _repository.GetAsync(d => d.OrganizationEventCommentId == contentId); + return document?.Reactions.Select(r => r.AsEntity()) ?? Enumerable.Empty(); + } + + public async Task GetAsync(Guid contentId, Guid userId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(x => x.OrganizationEventCommentId, contentId), + Builders.Filter.ElemMatch(x => x.Reactions, r => r.UserId == userId) + ); + + var document = await _repository.Collection.Find(filter).FirstOrDefaultAsync(); + return document?.Reactions.FirstOrDefault(r => r.UserId == userId)?.AsEntity(); + } + } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsEventMongoRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsEventMongoRepository.cs index 4743e54e8..ab76870f4 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsEventMongoRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsEventMongoRepository.cs @@ -30,12 +30,22 @@ public async Task GetByIdAsync(Guid id) public async Task AddAsync(Reaction reaction) { var filter = Builders.Filter.Eq(d => d.OrganizationEventId, reaction.ContentId); - var update = Builders.Update - .Push(d => d.Reactions, reaction.AsDocument()); - await _repository.Collection.UpdateOneAsync(filter, update); + var update = Builders.Update.Combine( + Builders.Update.Push(d => d.Reactions, reaction.AsDocument()), + Builders.Update.SetOnInsert(d => d.OrganizationEventId, reaction.ContentId), + Builders.Update.SetOnInsert(d => d.Id, Guid.NewGuid()) + ); + + var options = new UpdateOptions { IsUpsert = true }; + var result = await _repository.Collection.UpdateOneAsync(filter, update, options); + + if (!result.IsAcknowledged || result.ModifiedCount == 0) + { + } } + public async Task UpdateAsync(Reaction reaction) { var filter = Builders.Filter.And( @@ -57,5 +67,22 @@ public async Task DeleteAsync(Guid id) await _repository.Collection.UpdateOneAsync(filter, update); } + + public async Task> GetByContentIdAsync(Guid contentId) + { + var document = await _repository.GetAsync(d => d.OrganizationEventId == contentId); + return document?.Reactions.Select(r => r.AsEntity()) ?? Enumerable.Empty(); + } + + public async Task GetAsync(Guid contentId, Guid userId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(x => x.OrganizationEventId, contentId), + Builders.Filter.ElemMatch(x => x.Reactions, r => r.UserId == userId) + ); + + var document = await _repository.Collection.Find(filter).FirstOrDefaultAsync(); + return document?.Reactions.FirstOrDefault(r => r.UserId == userId)?.AsEntity(); + } } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsPostCommentsMongoRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsPostCommentsMongoRepository.cs index f591d57e7..897a1c769 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsPostCommentsMongoRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsPostCommentsMongoRepository.cs @@ -29,10 +29,20 @@ public async Task GetByIdAsync(Guid id) public async Task AddAsync(Reaction reaction) { - var filter = Builders.Filter.Eq(x => x.OrganizationPostCommentId, reaction.ContentId); - var update = Builders.Update.Push(x => x.Reactions, reaction.AsDocument()); + var filter = Builders.Filter.Eq(d => d.OrganizationPostCommentId, reaction.ContentId); - await _repository.Collection.UpdateOneAsync(filter, update); + var update = Builders.Update.Combine( + Builders.Update.Push(d => d.Reactions, reaction.AsDocument()), + Builders.Update.SetOnInsert(d => d.OrganizationPostCommentId, reaction.ContentId), + Builders.Update.SetOnInsert(d => d.Id, Guid.NewGuid()) + ); + + var options = new UpdateOptions { IsUpsert = true }; + var result = await _repository.Collection.UpdateOneAsync(filter, update, options); + + if (!result.IsAcknowledged || result.ModifiedCount == 0) + { + } } public async Task UpdateAsync(Reaction reaction) @@ -54,5 +64,22 @@ public async Task DeleteAsync(Guid id) await _repository.Collection.UpdateOneAsync(filter, update); } + + public async Task> GetByContentIdAsync(Guid contentId) + { + var document = await _repository.GetAsync(d => d.OrganizationPostCommentId == contentId); + return document?.Reactions.Select(r => r.AsEntity()) ?? Enumerable.Empty(); + } + + public async Task GetAsync(Guid contentId, Guid userId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(x => x.OrganizationPostCommentId, contentId), + Builders.Filter.ElemMatch(x => x.Reactions, r => r.UserId == userId) + ); + + var document = await _repository.Collection.Find(filter).FirstOrDefaultAsync(); + return document?.Reactions.FirstOrDefault(r => r.UserId == userId)?.AsEntity(); + } } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsPostMongoRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsPostMongoRepository.cs index 3ab31fc7b..ba3c10d71 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsPostMongoRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsOrganizationsPostMongoRepository.cs @@ -30,10 +30,19 @@ public async Task GetByIdAsync(Guid id) public async Task AddAsync(Reaction reaction) { var filter = Builders.Filter.Eq(d => d.OrganizationPostId, reaction.ContentId); - var update = Builders.Update - .Push(d => d.Reactions, reaction.AsDocument()); - await _repository.Collection.UpdateOneAsync(filter, update); + var update = Builders.Update.Combine( + Builders.Update.Push(d => d.Reactions, reaction.AsDocument()), + Builders.Update.SetOnInsert(d => d.OrganizationPostId, reaction.ContentId), + Builders.Update.SetOnInsert(d => d.Id, Guid.NewGuid()) + ); + + var options = new UpdateOptions { IsUpsert = true }; + var result = await _repository.Collection.UpdateOneAsync(filter, update, options); + + if (!result.IsAcknowledged || result.ModifiedCount == 0) + { + } } public async Task UpdateAsync(Reaction reaction) @@ -57,5 +66,22 @@ public async Task DeleteAsync(Guid id) await _repository.Collection.UpdateOneAsync(filter, update); } + + public async Task> GetByContentIdAsync(Guid contentId) + { + var document = await _repository.GetAsync(d => d.OrganizationPostId == contentId); + return document?.Reactions.Select(r => r.AsEntity()) ?? Enumerable.Empty(); + } + + public async Task GetAsync(Guid contentId, Guid userId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(x => x.OrganizationPostId, contentId), + Builders.Filter.ElemMatch(x => x.Reactions, r => r.UserId == userId) + ); + + var document = await _repository.Collection.Find(filter).FirstOrDefaultAsync(); + return document?.Reactions.FirstOrDefault(r => r.UserId == userId)?.AsEntity(); + } } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserEventCommentsMongoRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserEventCommentsMongoRepository.cs index 7e298d3de..aad68a002 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserEventCommentsMongoRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserEventCommentsMongoRepository.cs @@ -29,12 +29,23 @@ public async Task GetByIdAsync(Guid id) public async Task AddAsync(Reaction reaction) { - var filter = Builders.Filter.Eq(x => x.UserEventCommentId, reaction.ContentId); - var update = Builders.Update.Push(x => x.Reactions, reaction.AsDocument()); + var filter = Builders.Filter.Eq(d => d.UserEventCommentId, reaction.ContentId); - await _repository.Collection.UpdateOneAsync(filter, update); + var update = Builders.Update.Combine( + Builders.Update.Push(d => d.Reactions, reaction.AsDocument()), + Builders.Update.SetOnInsert(d => d.UserEventCommentId, reaction.ContentId), + Builders.Update.SetOnInsert(d => d.Id, Guid.NewGuid()) + ); + + var options = new UpdateOptions { IsUpsert = true }; + var result = await _repository.Collection.UpdateOneAsync(filter, update, options); + + if (!result.IsAcknowledged || result.ModifiedCount == 0) + { + } } + public async Task UpdateAsync(Reaction reaction) { var filter = Builders.Filter.And( @@ -54,5 +65,22 @@ public async Task DeleteAsync(Guid id) await _repository.Collection.UpdateOneAsync(filter, update); } + + public async Task> GetByContentIdAsync(Guid contentId) + { + var document = await _repository.GetAsync(d => d.UserEventCommentId == contentId); + return document?.Reactions.Select(r => r.AsEntity()) ?? Enumerable.Empty(); + } + + public async Task GetAsync(Guid contentId, Guid userId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(x => x.UserEventCommentId, contentId), + Builders.Filter.ElemMatch(x => x.Reactions, r => r.UserId == userId) + ); + + var document = await _repository.Collection.Find(filter).FirstOrDefaultAsync(); + return document?.Reactions.FirstOrDefault(r => r.UserId == userId)?.AsEntity(); + } } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserEventMongoRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserEventMongoRepository.cs index 91e8881f6..94787f938 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserEventMongoRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserEventMongoRepository.cs @@ -30,12 +30,22 @@ public async Task GetByIdAsync(Guid id) public async Task AddAsync(Reaction reaction) { var filter = Builders.Filter.Eq(d => d.UserEventId, reaction.ContentId); - var update = Builders.Update - .Push(d => d.Reactions, reaction.AsDocument()); - await _repository.Collection.UpdateOneAsync(filter, update); + var update = Builders.Update.Combine( + Builders.Update.Push(d => d.Reactions, reaction.AsDocument()), + Builders.Update.SetOnInsert(d => d.UserEventId, reaction.ContentId), + Builders.Update.SetOnInsert(d => d.Id, Guid.NewGuid()) // Ensure the document Id is a new Guid + ); + + var options = new UpdateOptions { IsUpsert = true }; + var result = await _repository.Collection.UpdateOneAsync(filter, update, options); + + if (!result.IsAcknowledged || result.ModifiedCount == 0) + { + } } + public async Task UpdateAsync(Reaction reaction) { var filter = Builders.Filter.And( @@ -57,5 +67,22 @@ public async Task DeleteAsync(Guid id) await _repository.Collection.UpdateOneAsync(filter, update); } + + public async Task> GetByContentIdAsync(Guid contentId) + { + var document = await _repository.GetAsync(d => d.UserEventId == contentId); + return document?.Reactions.Select(r => r.AsEntity()) ?? Enumerable.Empty(); + } + + public async Task GetAsync(Guid contentId, Guid userId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(x => x.UserEventId, contentId), + Builders.Filter.ElemMatch(x => x.Reactions, r => r.UserId == userId) + ); + + var document = await _repository.Collection.Find(filter).FirstOrDefaultAsync(); + return document?.Reactions.FirstOrDefault(r => r.UserId == userId)?.AsEntity(); + } } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserPostCommentsMongoRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserPostCommentsMongoRepository.cs index 73006147d..ca207a328 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserPostCommentsMongoRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserPostCommentsMongoRepository.cs @@ -27,14 +27,30 @@ public async Task GetByIdAsync(Guid id) return document?.Reactions.FirstOrDefault(r => r.Id == id)?.AsEntity(); } - public async Task AddAsync(Reaction reaction) + public async Task AddAsync(Reaction reaction) { + // Ensure the document's Id is set to the reaction's Id var filter = Builders.Filter.Eq(x => x.UserPostCommentId, reaction.ContentId); - var update = Builders.Update.Push(x => x.Reactions, reaction.AsDocument()); - await _repository.Collection.UpdateOneAsync(filter, update); + var update = Builders.Update.Combine( + Builders.Update.Push(x => x.Reactions, reaction.AsDocument()), + Builders.Update.SetOnInsert(x => x.UserPostCommentId, reaction.ContentId), + Builders.Update.SetOnInsert(x => x.Id, reaction.ContentId) // Set the Id to ensure it's not an ObjectId + ); + + var options = new UpdateOptions { IsUpsert = true }; // Upsert: insert if not exists, update if exists + var result = await _repository.Collection.UpdateOneAsync(filter, update, options); + + if (!result.IsAcknowledged || result.ModifiedCount == 0) + { + // Handle the case where the update wasn't acknowledged or nothing was modified + // This could involve logging or throwing an exception based on your application's needs + } } + + + public async Task UpdateAsync(Reaction reaction) { var filter = Builders.Filter.And( @@ -54,5 +70,22 @@ public async Task DeleteAsync(Guid id) await _repository.Collection.UpdateOneAsync(filter, update); } + + public async Task> GetByContentIdAsync(Guid contentId) + { + var document = await _repository.GetAsync(d => d.UserPostCommentId == contentId); + return document?.Reactions.Select(r => r.AsEntity()) ?? Enumerable.Empty(); + } + + public async Task GetAsync(Guid contentId, Guid userId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(x => x.UserPostCommentId, contentId), + Builders.Filter.ElemMatch(x => x.Reactions, r => r.UserId == userId) + ); + + var document = await _repository.Collection.Find(filter).FirstOrDefaultAsync(); + return document?.Reactions.FirstOrDefault(r => r.UserId == userId)?.AsEntity(); + } } } diff --git a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserPostMongoRepository.cs b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserPostMongoRepository.cs index 66e03855a..ec55ed5bc 100644 --- a/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserPostMongoRepository.cs +++ b/MiniSpace.Services.Reactions/src/MiniSpace.Services.Reactions.Infrastructure/Mongo/Repositories/ReactionsUserPostMongoRepository.cs @@ -30,9 +30,21 @@ public async Task GetByIdAsync(Guid id) public async Task AddAsync(Reaction reaction) { var filter = Builders.Filter.Eq(x => x.UserPostId, reaction.ContentId); - var update = Builders.Update.Push(x => x.Reactions, reaction.AsDocument()); - await _repository.Collection.UpdateOneAsync(filter, update); + var update = Builders.Update.Combine( + Builders.Update.Push(x => x.Reactions, reaction.AsDocument()), + Builders.Update.SetOnInsert(x => x.UserPostId, reaction.ContentId), + Builders.Update.SetOnInsert(x => x.Id, Guid.NewGuid()) // Ensure the document Id is a new Guid + ); + + var options = new UpdateOptions { IsUpsert = true }; // Upsert: insert if not exists, update if exists + var result = await _repository.Collection.UpdateOneAsync(filter, update, options); + + if (!result.IsAcknowledged || result.ModifiedCount == 0) + { + // Handle the case where the update wasn't acknowledged or nothing was modified + // This could involve logging or throwing an exception based on your application's needs + } } public async Task UpdateAsync(Reaction reaction) @@ -54,5 +66,25 @@ public async Task DeleteAsync(Guid id) await _repository.Collection.UpdateOneAsync(filter, update); } + + public async Task> GetByContentIdAsync(Guid contentId) + { + var filter = Builders.Filter.Eq(d => d.UserPostId, contentId); + var document = await _repository.Collection.Find(filter).FirstOrDefaultAsync(); + + return document?.Reactions.Select(r => r.AsEntity()) ?? Enumerable.Empty(); + } + + public async Task GetAsync(Guid contentId, Guid userId) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(x => x.UserPostId, contentId), + Builders.Filter.ElemMatch(x => x.Reactions, r => r.UserId == userId) + ); + + var document = await _repository.Collection.Find(filter).FirstOrDefaultAsync(); + return document?.Reactions.FirstOrDefault(r => r.UserId == userId)?.AsEntity(); + } + } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs index 5e55a750a..97903c2cb 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Api/Program.cs @@ -14,6 +14,7 @@ using MiniSpace.Services.Students.Application.Commands; using MiniSpace.Services.Students.Application.Dto; using MiniSpace.Services.Students.Application.Queries; +using MiniSpace.Services.Students.Core.Wrappers; using MiniSpace.Services.Students.Infrastructure; namespace MiniSpace.Services.Students.Api @@ -39,7 +40,11 @@ public static async Task Main(string[] args) .Get("students/{studentId}/visibility-settings") .Get("students/{studentId}/events") .Get("students/{studentId}/notifications") - + .Get>("students/profiles/users/{userId}/views/paginated") + .Get>("students/profiles/users/{userId}/views/viewed") + .Get>("students/{blockerId}/blocked-users") + + .Put("students/{studentId}") .Put("students/{studentId}/settings") .Put("students/{studentId}/state/{state}", @@ -47,11 +52,16 @@ public static async Task Main(string[] args) .Put("students/{studentId}/languages-and-interests") .Delete("students/{studentId}") - + + .Post("students/{blockerId}/block-user/{blockedUserId}", + afterDispatch: (cmd, ctx) => ctx.Response.Ok()) + .Post("students/{blockerId}/unblock-user/{blockedUserId}", + afterDispatch: (cmd, ctx) => ctx.Response.Ok()) + .Post("students", afterDispatch: (cmd, ctx) => ctx.Response.Created($"students/{cmd.StudentId}")) .Post("students/{studentId}/notifications") - + .Post("students/profiles/users/{userProfileId}/view", afterDispatch: (cmd, ctx) => ctx.Response.Ok()) )) .UseLogging() .Build() diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/BlockUser.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/BlockUser.cs new file mode 100644 index 000000000..c5641193f --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/BlockUser.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Commands; +using System; + +namespace MiniSpace.Services.Students.Application.Commands +{ + public class BlockUser : ICommand + { + public Guid BlockerId { get; } + public Guid BlockedUserId { get; } + + public BlockUser(Guid blockerId, Guid blockedUserId) + { + BlockerId = blockerId; + BlockedUserId = blockedUserId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/BlockUserHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/BlockUserHandler.cs new file mode 100644 index 000000000..6a4b53bf7 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/BlockUserHandler.cs @@ -0,0 +1,62 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Application.Commands.Handlers +{ + public class BlockUserHandler : ICommandHandler + { + private readonly IBlockedUsersRepository _blockedUsersRepository; + private readonly IStudentRepository _studentRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public BlockUserHandler(IBlockedUsersRepository blockedUsersRepository, IStudentRepository studentRepository, IEventMapper eventMapper, IMessageBroker messageBroker) + { + _blockedUsersRepository = blockedUsersRepository; + _studentRepository = studentRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(BlockUser command, CancellationToken cancellationToken = default) + { + // Ensure the blocker exists + var blocker = await _studentRepository.GetAsync(command.BlockerId); + if (blocker is null) + { + throw new StudentNotFoundException(command.BlockerId); + } + + // Ensure the user to be blocked exists + var blockedUser = await _studentRepository.GetAsync(command.BlockedUserId); + if (blockedUser is null) + { + throw new StudentNotFoundException(command.BlockedUserId); + } + + // Fetch or create the BlockedUsers aggregate + var blockedUsersAggregate = await _blockedUsersRepository.GetAsync(command.BlockerId); + if (blockedUsersAggregate == null) + { + blockedUsersAggregate = new BlockedUsers(command.BlockerId); + await _blockedUsersRepository.AddAsync(blockedUsersAggregate); + } + + // Block the user + blockedUsersAggregate.BlockUser(command.BlockedUserId); + + // Update the repository to save changes + await _blockedUsersRepository.UpdateAsync(blockedUsersAggregate); + + // Publish domain events + var events = _eventMapper.MapAll(blockedUsersAggregate.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UnblockUserHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UnblockUserHandler.cs new file mode 100644 index 000000000..75ae90a07 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/UnblockUserHandler.cs @@ -0,0 +1,65 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.Students.Application.Exceptions; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Application.Commands.Handlers +{ + public class UnblockUserHandler : ICommandHandler + { + private readonly IBlockedUsersRepository _blockedUsersRepository; + private readonly IStudentRepository _studentRepository; + private readonly IEventMapper _eventMapper; + private readonly IMessageBroker _messageBroker; + + public UnblockUserHandler(IBlockedUsersRepository blockedUsersRepository, IStudentRepository studentRepository, IEventMapper eventMapper, IMessageBroker messageBroker) + { + _blockedUsersRepository = blockedUsersRepository; + _studentRepository = studentRepository; + _eventMapper = eventMapper; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(UnblockUser command, CancellationToken cancellationToken = default) + { + var blocker = await _studentRepository.GetAsync(command.BlockerId); + var blockedUser = await _studentRepository.GetAsync(command.BlockedUserId); + + if (blocker is null) + { + throw new StudentNotFoundException(command.BlockerId); + } + + if (blockedUser is null) + { + throw new StudentNotFoundException(command.BlockedUserId); + } + + var blockedUsersAggregate = await _blockedUsersRepository.GetAsync(command.BlockerId); + + if (blockedUsersAggregate == null || !blockedUsersAggregate.BlockedUsersList.Any(b => b.BlockedUserId == command.BlockedUserId)) + { + throw new UserNotBlockedException(command.BlockerId, command.BlockedUserId); + } + + blockedUsersAggregate.UnblockUser(command.BlockedUserId); + + if (!blockedUsersAggregate.BlockedUsersList.Any()) + { + await _blockedUsersRepository.DeleteAsync(command.BlockerId); + } + else + { + await _blockedUsersRepository.UpdateAsync(blockedUsersAggregate); + } + + var events = _eventMapper.MapAll(blockedUsersAggregate.Events); + await _messageBroker.PublishAsync(events.ToArray()); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/ViewUserProfileHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/ViewUserProfileHandler.cs new file mode 100644 index 000000000..ea4330070 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/Handlers/ViewUserProfileHandler.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Http; +using MiniSpace.Services.Students.Application.Commands; +using MiniSpace.Services.Students.Application.Services; + +namespace MiniSpace.Services.Students.Application.Commands.Handlers +{ + public class ViewUserProfileHandler : ICommandHandler + { + private readonly IUserProfileViewsForUserRepository _userProfileViewsForUserRepository; + private readonly IUserViewingProfilesRepository _userViewingProfilesRepository; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IDeviceInfoService _deviceInfoService; + private readonly ILogger _logger; + + public ViewUserProfileHandler( + IUserProfileViewsForUserRepository userProfileViewsForUserRepository, + IUserViewingProfilesRepository userViewingProfilesRepository, + IHttpContextAccessor httpContextAccessor, + IDeviceInfoService deviceInfoService, + ILogger logger) + { + _userProfileViewsForUserRepository = userProfileViewsForUserRepository; + _userViewingProfilesRepository = userViewingProfilesRepository; + _httpContextAccessor = httpContextAccessor; + _deviceInfoService = deviceInfoService; + _logger = logger; + } + + public async Task HandleAsync(ViewUserProfile command, CancellationToken cancellationToken) + { + var httpContext = _httpContextAccessor.HttpContext; + var deviceInfo = _deviceInfoService.GetDeviceInfo(httpContext); + + // Handle views of the user profile being viewed + var userViewsForUser = await _userProfileViewsForUserRepository.GetAsync(command.UserProfileId); + if (userViewsForUser == null) + { + userViewsForUser = new UserProfileViewsForUser(command.UserProfileId, Enumerable.Empty()); + } + + userViewsForUser.AddView(command.UserId, DateTime.UtcNow, deviceInfo.IpAddress, deviceInfo.DeviceType, deviceInfo.OperatingSystem); + await _userProfileViewsForUserRepository.UpdateAsync(userViewsForUser); + + // Handle views by the user viewing profiles + var userViewingProfiles = await _userViewingProfilesRepository.GetAsync(command.UserId); + if (userViewingProfiles == null) + { + userViewingProfiles = new UserViewingProfiles(command.UserId, Enumerable.Empty()); + } + + userViewingProfiles.AddViewedProfile(command.UserProfileId, DateTime.UtcNow, deviceInfo.IpAddress, deviceInfo.DeviceType, deviceInfo.OperatingSystem); + await _userViewingProfilesRepository.UpdateAsync(userViewingProfiles); + + _logger.LogInformation($"User {command.UserId} viewed user profile {command.UserProfileId} from IP {deviceInfo.IpAddress} using {deviceInfo.DeviceType} ({deviceInfo.OperatingSystem})."); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UnblockUser.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UnblockUser.cs new file mode 100644 index 000000000..f34dc7f3b --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UnblockUser.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Commands; +using System; + +namespace MiniSpace.Services.Students.Application.Commands +{ + public class UnblockUser : ICommand + { + public Guid BlockerId { get; } + public Guid BlockedUserId { get; } + + public UnblockUser(Guid blockerId, Guid blockedUserId) + { + BlockerId = blockerId; + BlockedUserId = blockedUserId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/ViewUserProfile.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/ViewUserProfile.cs new file mode 100644 index 000000000..9f3c9c8a3 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/ViewUserProfile.cs @@ -0,0 +1,17 @@ +using System; +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.Students.Application.Commands +{ + public class ViewUserProfile : ICommand + { + public Guid UserId { get; } + public Guid UserProfileId { get; } + + public ViewUserProfile(Guid userId, Guid userProfileId) + { + UserId = userId; + UserProfileId = userProfileId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/BlockedUserDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/BlockedUserDto.cs new file mode 100644 index 000000000..923ca97e4 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/BlockedUserDto.cs @@ -0,0 +1,9 @@ +namespace MiniSpace.Services.Students.Application.Dto +{ + public class BlockedUserDto + { + public Guid BlockerId { get; set; } + public Guid BlockedUserId { get; set; } + public DateTime BlockedAt { get; set; } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/UserProfileViewDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/UserProfileViewDto.cs new file mode 100644 index 000000000..b9639c9fa --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/UserProfileViewDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Application.Dto +{ + public class UserProfileViewDto + { + public Guid UserProfileId { get; set; } + public DateTime Date { get; set; } + public string IpAddress { get; set; } + public string DeviceType { get; set; } + public string OperatingSystem { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/UserBlocked.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/UserBlocked.cs new file mode 100644 index 000000000..120e49050 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/UserBlocked.cs @@ -0,0 +1,17 @@ +using Convey.CQRS.Events; +using System; + +namespace MiniSpace.Services.Students.Application.Events +{ + public class UserBlocked : IEvent + { + public Guid BlockerId { get; } + public Guid BlockedUserId { get; } + + public UserBlocked(Guid blockerId, Guid blockedUserId) + { + BlockerId = blockerId; + BlockedUserId = blockedUserId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UserAlreadyBlockedException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UserAlreadyBlockedException.cs new file mode 100644 index 000000000..41bd5abd9 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UserAlreadyBlockedException.cs @@ -0,0 +1,12 @@ +namespace MiniSpace.Services.Students.Application.Exceptions +{ + public class UserAlreadyBlockedException : AppException + { + public override string Code { get; } = "user_already_blocked"; + + public UserAlreadyBlockedException(Guid blockerId, Guid blockedUserId) + : base($"User with ID: '{blockedUserId}' is already blocked by user with ID: '{blockerId}'.") + { + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UserNotBlockedException.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UserNotBlockedException.cs new file mode 100644 index 000000000..0d2baced3 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Exceptions/UserNotBlockedException.cs @@ -0,0 +1,14 @@ +using System; + +namespace MiniSpace.Services.Students.Application.Exceptions +{ + public class UserNotBlockedException : AppException + { + public override string Code { get; } = "user_not_blocked"; + + public UserNotBlockedException(Guid blockerId, Guid blockedUserId) + : base($"User with ID '{blockedUserId}' is not blocked by user with ID '{blockerId}'.") + { + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetBlockedUsers.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetBlockedUsers.cs new file mode 100644 index 000000000..95ef7c7d9 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetBlockedUsers.cs @@ -0,0 +1,26 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Core.Wrappers; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Application.Queries +{ + [ExcludeFromCodeCoverage] + public class GetBlockedUsers : IQuery> + { + public Guid BlockerId { get; set; } + public int Page { get; set; } + public int ResultsPerPage { get; set; } + public string OrderBy { get; set; } + public string SortOrder { get; set; } + + public GetBlockedUsers(Guid blockerId, int page, int resultsPerPage, string orderBy = "BlockedAt", string sortOrder = "desc") + { + BlockerId = blockerId; + Page = page; + ResultsPerPage = resultsPerPage; + OrderBy = orderBy; + SortOrder = sortOrder; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetProfilesViewedByUser.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetProfilesViewedByUser.cs new file mode 100644 index 000000000..9602600d4 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetProfilesViewedByUser.cs @@ -0,0 +1,21 @@ +using System; +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Core.Wrappers; + +namespace MiniSpace.Services.Students.Application.Queries +{ + public class GetProfilesViewedByUser : IQuery> + { + public Guid UserId { get; } + public int PageNumber { get; } + public int PageSize { get; } + + public GetProfilesViewedByUser(Guid userId, int pageNumber = 1, int pageSize = 10) + { + UserId = userId; + PageNumber = pageNumber; + PageSize = pageSize; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetUserProfileViews.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetUserProfileViews.cs new file mode 100644 index 000000000..dd8344685 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Queries/GetUserProfileViews.cs @@ -0,0 +1,21 @@ +using System; +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Core.Wrappers; + +namespace MiniSpace.Services.Students.Application.Queries +{ + public class GetUserProfileViews : IQuery> + { + public Guid UserId { get; } + public int PageNumber { get; } + public int PageSize { get; } + + public GetUserProfileViews(Guid userId, int pageNumber = 1, int pageSize = 10) + { + UserId = userId; + PageNumber = pageNumber; + PageSize = pageSize; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Services/IDeviceInfoService.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Services/IDeviceInfoService.cs new file mode 100644 index 000000000..f7272f6d7 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Services/IDeviceInfoService.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Http; +using MiniSpace.Services.Students.Core.Entities; +using System; + +namespace MiniSpace.Services.Students.Application.Services +{ + public interface IDeviceInfoService + { + DeviceInfo GetDeviceInfo(HttpContext httpContext); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/BlockedUser.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/BlockedUser.cs new file mode 100644 index 000000000..d62c754f6 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/BlockedUser.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class BlockedUser + { + public Guid BlockerId { get; private set; } + public Guid BlockedUserId { get; private set; } + public DateTime BlockedAt { get; private set; } + + public BlockedUser(Guid blockerId, Guid blockedUserId, DateTime blockedAt) + { + BlockerId = blockerId; + BlockedUserId = blockedUserId; + BlockedAt = blockedAt; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/BlockedUsers.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/BlockedUsers.cs new file mode 100644 index 000000000..fa23caf66 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/BlockedUsers.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MiniSpace.Services.Students.Core.Events; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class BlockedUsers : AggregateRoot + { + private readonly ISet _blockedUsers = new HashSet(); + + public BlockedUsers(Guid userId) + { + Id = userId; + UserId = userId; + } + + public Guid UserId { get; private set; } + public IEnumerable BlockedUsersList => _blockedUsers; + + public void BlockUser(Guid blockedUserId) + { + if (blockedUserId == Guid.Empty || _blockedUsers.Any(b => b.BlockedUserId == blockedUserId)) + { + throw new InvalidOperationException($"User with ID {blockedUserId} cannot be blocked or is already blocked."); + } + + var blockedUser = new BlockedUser(UserId, blockedUserId, DateTime.UtcNow); + _blockedUsers.Add(blockedUser); + AddEvent(new UserBlockedEvent(this, blockedUserId)); + } + + public void UnblockUser(Guid blockedUserId) + { + var blockedUser = _blockedUsers.SingleOrDefault(b => b.BlockedUserId == blockedUserId); + if (blockedUser == null) + { + throw new InvalidOperationException($"User with ID {blockedUserId} is not blocked."); + } + + _blockedUsers.Remove(blockedUser); + AddEvent(new UserUnblockedEvent(this, blockedUserId)); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/DeviceInfo.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/DeviceInfo.cs new file mode 100644 index 000000000..19cc88e00 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/DeviceInfo.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class DeviceInfo + { + public string IpAddress { get; set; } + public string DeviceType { get; set; } + public string OperatingSystem { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs index 0bbb02781..5a6cfde4c 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs @@ -14,7 +14,6 @@ public class Student : AggregateRoot private ISet _interests = new HashSet(); private ISet _education = new HashSet(); private ISet _work = new HashSet(); - public string Email { get; private set; } public string FirstName { get; private set; } public string LastName { get; private set; } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserProfileView.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserProfileView.cs new file mode 100644 index 000000000..0970c8eff --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserProfileView.cs @@ -0,0 +1,22 @@ +using System; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class UserProfileView + { + public Guid UserProfileId { get; private set; } + public DateTime Date { get; private set; } + public string IpAddress { get; private set; } + public string DeviceType { get; private set; } + public string OperatingSystem { get; private set; } + + public UserProfileView(Guid userProfileId, DateTime date, string ipAddress, string deviceType, string operatingSystem) + { + UserProfileId = userProfileId; + Date = date; + IpAddress = ipAddress; + DeviceType = deviceType; + OperatingSystem = operatingSystem; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserProfileViewsForUser.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserProfileViewsForUser.cs new file mode 100644 index 000000000..1b9655e09 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserProfileViewsForUser.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class UserProfileViewsForUser + { + public Guid UserId { get; private set; } + public IEnumerable Views { get; private set; } + + public UserProfileViewsForUser(Guid userId, IEnumerable views) + { + UserId = userId; + Views = views ?? new List(); + } + + public void AddView(Guid userProfileId, DateTime date, string ipAddress, string deviceType, string operatingSystem) + { + var viewList = new List(Views) + { + new UserProfileView(userProfileId, date, ipAddress, deviceType, operatingSystem) + }; + Views = viewList; + } + + public void RemoveView(Guid userProfileId) + { + var viewList = new List(Views); + var viewToRemove = viewList.Find(view => view.UserProfileId == userProfileId); + if (viewToRemove != null) + { + viewList.Remove(viewToRemove); + Views = viewList; + } + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserViewingProfiles.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserViewingProfiles.cs new file mode 100644 index 000000000..b9b536809 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/UserViewingProfiles.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MiniSpace.Services.Students.Core.Entities +{ + public class UserViewingProfiles + { + public Guid UserId { get; private set; } + public IEnumerable ViewedProfiles { get; private set; } + + public UserViewingProfiles(Guid userId, IEnumerable viewedProfiles) + { + UserId = userId; + ViewedProfiles = viewedProfiles ?? new List(); + } + + public void AddViewedProfile(Guid userProfileId, DateTime date, string ipAddress, string deviceType, string operatingSystem) + { + var viewedList = new List(ViewedProfiles) + { + new UserProfileView(userProfileId, date, ipAddress, deviceType, operatingSystem) + }; + ViewedProfiles = viewedList; + } + + public void RemoveViewedProfile(Guid userProfileId) + { + var viewedList = new List(ViewedProfiles); + var profileToRemove = viewedList.Find(view => view.UserProfileId == userProfileId); + if (profileToRemove != null) + { + viewedList.Remove(profileToRemove); + ViewedProfiles = viewedList; + } + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/UserBlockedEvent.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/UserBlockedEvent.cs new file mode 100644 index 000000000..0b3f0d8d4 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/UserBlockedEvent.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Events +{ + public class UserBlockedEvent : IDomainEvent + { + public BlockedUsers BlockedUsers { get; } + public Guid BlockedUserId { get; } + + public UserBlockedEvent(BlockedUsers blockedUsers, Guid blockedUserId) + { + BlockedUsers = blockedUsers; + BlockedUserId = blockedUserId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/UserUnblockedEvent.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/UserUnblockedEvent.cs new file mode 100644 index 000000000..e7dab95c8 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Events/UserUnblockedEvent.cs @@ -0,0 +1,16 @@ +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Events +{ + public class UserUnblockedEvent : IDomainEvent + { + public BlockedUsers BlockedUsers { get; } + public Guid UnblockedUserId { get; } + + public UserUnblockedEvent(BlockedUsers blockedUsers, Guid unblockedUserId) + { + BlockedUsers = blockedUsers; + UnblockedUserId = unblockedUserId; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IBlockedUsersRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IBlockedUsersRepository.cs new file mode 100644 index 000000000..5c541bffc --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IBlockedUsersRepository.cs @@ -0,0 +1,14 @@ +using MiniSpace.Services.Students.Core.Entities; +using System; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Core.Repositories +{ + public interface IBlockedUsersRepository + { + Task GetAsync(Guid userId); + Task AddAsync(BlockedUsers blockedUsers); + Task UpdateAsync(BlockedUsers blockedUsers); + Task DeleteAsync(Guid userId); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserProfileViewsForUserRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserProfileViewsForUserRepository.cs new file mode 100644 index 000000000..a2bbe3c41 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserProfileViewsForUserRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Repositories +{ + public interface IUserProfileViewsForUserRepository + { + Task GetAsync(Guid userId); + Task AddAsync(UserProfileViewsForUser userProfileViews); + Task UpdateAsync(UserProfileViewsForUser userProfileViews); + Task DeleteAsync(Guid userId); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserViewingProfilesRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserViewingProfilesRepository.cs new file mode 100644 index 000000000..050fb7ee2 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Repositories/IUserViewingProfilesRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Core.Repositories +{ + public interface IUserViewingProfilesRepository + { + Task GetAsync(Guid userId); + Task AddAsync(UserViewingProfiles userViewingProfiles); + Task UpdateAsync(UserViewingProfiles userViewingProfiles); + Task DeleteAsync(Guid userId); + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Wrappers/PagedResponse.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Wrappers/PagedResponse.cs new file mode 100644 index 000000000..95197acb6 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Wrappers/PagedResponse.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Core.Wrappers +{ + public class PagedResponse + { + public IEnumerable Items { get; } + public int TotalPages { get; } + public int TotalItems { get; } + public int PageSize { get; } + public int Page { get; } + public bool First { get; } + public bool Last { get; } + public bool Empty { get; } + public int? NextPage => Page < TotalPages ? Page + 1 : (int?)null; + public int? PreviousPage => Page > 1 ? Page - 1 : (int?)null; + + public PagedResponse(IEnumerable items, int page, int pageSize, int totalItems) + { + Items = items; + PageSize = pageSize; + TotalItems = totalItems; + TotalPages = pageSize > 0 ? (int)Math.Ceiling((decimal)totalItems / pageSize) : 0; + Page = page; + First = page == 1; + Last = page == TotalPages; + Empty = !items.Any(); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs index d0e50d304..fb3b66923 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Extensions.cs @@ -54,9 +54,14 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(ctx => ctx.GetRequiredService().Create()); builder.Services.TryDecorate(typeof(ICommandHandler<>), typeof(OutboxCommandHandlerDecorator<>)); @@ -81,6 +86,9 @@ public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) .AddMongoRepository("user-notifications") .AddMongoRepository("user-settings") .AddMongoRepository("user-gellery") + .AddMongoRepository("user_profile_views") + .AddMongoRepository("user_viewing_profiles") + .AddMongoRepository("blocked_users") .AddWebApiSwaggerDocs() .AddCertificateAuthentication() .AddSecurity(); diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/BlockedUsersDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/BlockedUsersDocument.cs new file mode 100644 index 000000000..a1b568ede --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/BlockedUsersDocument.cs @@ -0,0 +1,21 @@ +using Convey.Types; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + [ExcludeFromCodeCoverage] + public class BlockedUsersDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + public IEnumerable BlockedUsers { get; set; } = new List(); + + public class BlockedUserEntry + { + public Guid BlockedUserId { get; set; } + public DateTime BlockedAt { get; set; } + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/BlockedUsersExtensions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/BlockedUsersExtensions.cs new file mode 100644 index 000000000..0953a9a27 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/BlockedUsersExtensions.cs @@ -0,0 +1,52 @@ +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using MiniSpace.Services.Students.Application.Dto; +using System.Linq; +using System.Collections.Generic; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + public static class BlockedUsersExtensions + { + public static BlockedUsersDocument AsDocument(this BlockedUsers blockedUsers) + { + return new BlockedUsersDocument + { + Id = blockedUsers.UserId, + UserId = blockedUsers.UserId, + BlockedUsers = blockedUsers.BlockedUsersList.Select(b => new BlockedUsersDocument.BlockedUserEntry + { + BlockedUserId = b.BlockedUserId, + BlockedAt = b.BlockedAt + }).ToList() + }; + } + + public static BlockedUsers AsEntity(this BlockedUsersDocument document) + { + var blockedUsers = new BlockedUsers(document.UserId); + foreach (var entry in document.BlockedUsers) + { + var blockedUser = new BlockedUser(document.UserId, entry.BlockedUserId, entry.BlockedAt); + blockedUsers.BlockUser(blockedUser.BlockedUserId); + } + + return blockedUsers; + } + + public static BlockedUserDto AsDto(this BlockedUsersDocument.BlockedUserEntry blockedUserEntry, Guid blockerId) + { + return new BlockedUserDto + { + BlockerId = blockerId, + BlockedUserId = blockedUserEntry.BlockedUserId, + BlockedAt = blockedUserEntry.BlockedAt + }; + } + + public static IEnumerable AsDto(this BlockedUsersDocument document) + { + return document.BlockedUsers.Select(b => b.AsDto(document.UserId)); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewDocument.cs new file mode 100644 index 000000000..4ef955c15 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewDocument.cs @@ -0,0 +1,34 @@ +using System; +using Convey.Types; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + public class UserProfileViewDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid UserProfileId { get; set; } + public DateTime Date { get; set; } + public string IpAddress { get; set; } + public string DeviceType { get; set; } + public string OperatingSystem { get; set; } + + public static UserProfileViewDocument FromEntity(UserProfileView view) + { + return new UserProfileViewDocument + { + Id = Guid.NewGuid(), + UserProfileId = view.UserProfileId, + Date = view.Date, + IpAddress = view.IpAddress, + DeviceType = view.DeviceType, + OperatingSystem = view.OperatingSystem + }; + } + + public UserProfileView ToEntity() + { + return new UserProfileView(UserProfileId, Date, IpAddress, DeviceType, OperatingSystem); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewsDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewsDocument.cs new file mode 100644 index 000000000..8050d1571 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewsDocument.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Convey.Types; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + public class UserProfileViewsDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + public List Views { get; set; } = new List(); + + public static UserProfileViewsDocument FromEntity(UserProfileViewsForUser userProfileViews) + { + return new UserProfileViewsDocument + { + Id = Guid.NewGuid(), + UserId = userProfileViews.UserId, + Views = userProfileViews.Views.Select(UserProfileViewDocument.FromEntity).ToList() + }; + } + + public UserProfileViewsForUser ToEntity() + { + return new UserProfileViewsForUser(UserId, Views.Select(view => view.ToEntity())); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewsExtensions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewsExtensions.cs new file mode 100644 index 000000000..1fb2795ff --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserProfileViewsExtensions.cs @@ -0,0 +1,39 @@ + +using System.Linq; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + public static class UserProfileViewsExtensions + { + public static UserProfileViewsDocument AsDocument(this UserProfileViewsForUser entity) + { + return UserProfileViewsDocument.FromEntity(entity); + } + + public static UserProfileViewsForUser AsEntity(this UserProfileViewsDocument document) + { + return document.ToEntity(); + } + + public static UserProfileViewDocument AsDocument(this UserProfileView view) + { + return UserProfileViewDocument.FromEntity(view); + } + + public static UserProfileView AsEntity(this UserProfileViewDocument document) + { + return document.ToEntity(); + } + + public static UserViewingProfilesDocument AsDocument(this UserViewingProfiles entity) + { + return UserViewingProfilesDocument.FromEntity(entity); + } + + public static UserViewingProfiles AsEntity(this UserViewingProfilesDocument document) + { + return document.ToEntity(); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserViewingProfilesDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserViewingProfilesDocument.cs new file mode 100644 index 000000000..fde8d2e2c --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/UserViewingProfilesDocument.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Convey.Types; +using MiniSpace.Services.Students.Core.Entities; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Documents +{ + public class UserViewingProfilesDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + public List ViewedProfiles { get; set; } = new List(); + + public static UserViewingProfilesDocument FromEntity(UserViewingProfiles userViewingProfiles) + { + return new UserViewingProfilesDocument + { + Id = Guid.NewGuid(), + UserId = userViewingProfiles.UserId, + ViewedProfiles = userViewingProfiles.ViewedProfiles.Select(UserProfileViewDocument.FromEntity).ToList() + }; + } + + public UserViewingProfiles ToEntity() + { + return new UserViewingProfiles(UserId, ViewedProfiles.Select(view => view.ToEntity())); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetBlockedUsersHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetBlockedUsersHandler.cs new file mode 100644 index 000000000..e02920025 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetBlockedUsersHandler.cs @@ -0,0 +1,59 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Application.Queries; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Core.Wrappers; + +namespace MiniSpace.Services.Students.Application.Queries.Handlers +{ + public class GetBlockedUsersHandler : IQueryHandler> + { + private readonly IBlockedUsersRepository _blockedUsersRepository; + + public GetBlockedUsersHandler(IBlockedUsersRepository blockedUsersRepository) + { + _blockedUsersRepository = blockedUsersRepository; + } + + public async Task> HandleAsync(GetBlockedUsers query, CancellationToken cancellationToken = default) + { + var blockedUsersAggregate = await _blockedUsersRepository.GetAsync(query.BlockerId); + + if (blockedUsersAggregate == null || !blockedUsersAggregate.BlockedUsersList.Any()) + { + return new PagedResponse( + Enumerable.Empty(), + query.Page, + query.ResultsPerPage, + 0); + } + + var sortedBlockedUsers = query.SortOrder.ToLower() == "asc" + ? blockedUsersAggregate.BlockedUsersList.OrderBy(b => b.BlockedAt) + : blockedUsersAggregate.BlockedUsersList.OrderByDescending(b => b.BlockedAt); + + var totalItems = sortedBlockedUsers.Count(); + var totalPages = (int)System.Math.Ceiling(totalItems / (double)query.ResultsPerPage); + + var blockedUsersPage = sortedBlockedUsers + .Skip((query.Page - 1) * query.ResultsPerPage) + .Take(query.ResultsPerPage) + .Select(b => new BlockedUserDto + { + BlockerId = query.BlockerId, + BlockedUserId = b.BlockedUserId, + BlockedAt = b.BlockedAt + }) + .ToList(); + + return new PagedResponse( + blockedUsersPage, + query.Page, + query.ResultsPerPage, + totalItems); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetProfilesViewedByUserHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetProfilesViewedByUserHandler.cs new file mode 100644 index 000000000..12b537591 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetProfilesViewedByUserHandler.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Core.Wrappers; + +namespace MiniSpace.Services.Students.Application.Queries.Handlers +{ + public class GetProfilesViewedByUserHandler : IQueryHandler> + { + private readonly IUserViewingProfilesRepository _userViewingProfilesRepository; + + public GetProfilesViewedByUserHandler(IUserViewingProfilesRepository userViewingProfilesRepository) + { + _userViewingProfilesRepository = userViewingProfilesRepository; + } + + public async Task> HandleAsync(GetProfilesViewedByUser query, CancellationToken cancellationToken) + { + var userViewingProfiles = await _userViewingProfilesRepository.GetAsync(query.UserId); + + if (userViewingProfiles == null || !userViewingProfiles.ViewedProfiles.Any()) + { + return new PagedResponse(Enumerable.Empty(), query.PageNumber, query.PageSize, 0); + } + + var totalItems = userViewingProfiles.ViewedProfiles.Count(); + var totalPages = (int)Math.Ceiling(totalItems / (double)query.PageSize); + + var pagedViews = userViewingProfiles.ViewedProfiles + .Skip((query.PageNumber - 1) * query.PageSize) + .Take(query.PageSize) + .Select(view => new UserProfileViewDto + { + UserProfileId = view.UserProfileId, + Date = view.Date, + IpAddress = view.IpAddress, + DeviceType = view.DeviceType, + OperatingSystem = view.OperatingSystem + }) + .ToList(); + + return new PagedResponse(pagedViews, query.PageNumber, query.PageSize, totalItems); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetUserProfileViewsHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetUserProfileViewsHandler.cs new file mode 100644 index 000000000..89a28990e --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Queries/Handlers/GetUserProfileViewsHandler.cs @@ -0,0 +1,58 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Queries; +using MiniSpace.Services.Students.Application.Dto; +using MiniSpace.Services.Students.Application.Queries; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Core.Wrappers; + +namespace MiniSpace.Services.Students.Application.Queries.Handlers +{ + public class GetUserProfileViewsHandler : IQueryHandler> + { + private readonly IUserProfileViewsForUserRepository _userProfileViewsRepository; + + public GetUserProfileViewsHandler(IUserProfileViewsForUserRepository userProfileViewsRepository) + { + _userProfileViewsRepository = userProfileViewsRepository; + } + + public async Task> HandleAsync(GetUserProfileViews query, CancellationToken cancellationToken = default) + { + var userProfileViews = await _userProfileViewsRepository.GetAsync(query.UserId); + + if (userProfileViews == null || !userProfileViews.Views.Any()) + { + return new PagedResponse( + Enumerable.Empty(), + query.PageNumber, + query.PageSize, + 0); + } + + var totalItems = userProfileViews.Views.Count(); + var totalPages = (int)System.Math.Ceiling(totalItems / (double)query.PageSize); + + var views = userProfileViews.Views + .Skip((query.PageNumber - 1) * query.PageSize) + .Take(query.PageSize) + .Select(view => new UserProfileViewDto + { + UserProfileId = view.UserProfileId, + Date = view.Date, + IpAddress = view.IpAddress, + DeviceType = view.DeviceType, + OperatingSystem = view.OperatingSystem + }); + + var pagedResponse = new PagedResponse( + views, + query.PageNumber, + query.PageSize, + totalItems); + + return pagedResponse; + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/BlockedUsersMongoRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/BlockedUsersMongoRepository.cs new file mode 100644 index 000000000..be16ef111 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/BlockedUsersMongoRepository.cs @@ -0,0 +1,54 @@ +using Convey.Persistence.MongoDB; +using MongoDB.Driver; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Repositories +{ + public class BlockedUsersMongoRepository : IBlockedUsersRepository + { + private readonly IMongoRepository _repository; + + public BlockedUsersMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid blockerId) + { + var document = await _repository.GetAsync(d => d.UserId == blockerId); + return document?.AsEntity(); + } + + public async Task AddAsync(BlockedUsers blockedUsers) + { + var document = blockedUsers.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task UpdateAsync(BlockedUsers blockedUsers) + { + var document = blockedUsers.AsDocument(); + var filter = Builders.Filter.Eq(d => d.UserId, document.UserId); + + // Replace the existing document + var result = await _repository.Collection.ReplaceOneAsync(filter, document, new ReplaceOptions { IsUpsert = true }); + + // Ensure the operation was successful + if (result.MatchedCount == 0 && result.UpsertedId == null) + { + throw new Exception("Failed to update the blocked user list."); + } + } + + public async Task DeleteAsync(Guid blockerId) + { + var filter = Builders.Filter.Eq(d => d.UserId, blockerId); + await _repository.Collection.DeleteOneAsync(filter); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserProfileViewsRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserProfileViewsRepository.cs new file mode 100644 index 000000000..f9d92fb43 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserProfileViewsRepository.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using Convey.Persistence.MongoDB; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Repositories +{ + public class UserProfileViewsRepository : IUserProfileViewsForUserRepository + { + private readonly IMongoRepository _repository; + + public UserProfileViewsRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid userId) + { + var document = await _repository.GetAsync(x => x.UserId == userId); + return document?.ToEntity(); + } + + public async Task AddAsync(UserProfileViewsForUser userProfileViews) + { + var document = userProfileViews.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task UpdateAsync(UserProfileViewsForUser userProfileViews) + { + var document = userProfileViews.AsDocument(); + await _repository.UpdateAsync(document); + } + + public async Task DeleteAsync(Guid userId) + { + await _repository.DeleteAsync(x => x.UserId == userId); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserViewingProfilesRepository.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserViewingProfilesRepository.cs new file mode 100644 index 000000000..e24526e2a --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Repositories/UserViewingProfilesRepository.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using MiniSpace.Services.Students.Core.Entities; +using MiniSpace.Services.Students.Core.Repositories; +using MiniSpace.Services.Students.Infrastructure.Mongo.Documents; +using Convey.Persistence.MongoDB; + +namespace MiniSpace.Services.Students.Infrastructure.Mongo.Repositories +{ + public class UserViewingProfilesRepository : IUserViewingProfilesRepository + { + private readonly IMongoRepository _repository; + + public UserViewingProfilesRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid userId) + { + var document = await _repository.GetAsync(x => x.UserId == userId); + return document?.ToEntity(); + } + + public async Task AddAsync(UserViewingProfiles userViewingProfiles) + { + var document = userViewingProfiles.AsDocument(); + await _repository.AddAsync(document); + } + + public async Task UpdateAsync(UserViewingProfiles userViewingProfiles) + { + var document = userViewingProfiles.AsDocument(); + await _repository.UpdateAsync(document); + } + + public async Task DeleteAsync(Guid userId) + { + await _repository.DeleteAsync(x => x.UserId == userId); + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Options/MongoDbOptions.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Options/MongoDbOptions.cs deleted file mode 100644 index 029c915f2..000000000 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Options/MongoDbOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MiniSpace.Services.Students.Infrastructure.Options -{ - public class MongoDbOptions - { - public string ConnectionString { get; set; } - public string WriteDatabase { get; set; } - public string ReadDatabase { get; set; } - public bool Seed { get; set; } - } -} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/DeviceInfoService.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/DeviceInfoService.cs new file mode 100644 index 000000000..bcaaa0eea --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/DeviceInfoService.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Http; +using MiniSpace.Services.Students.Application.Services; +using MiniSpace.Services.Students.Core.Entities; +using System; + +namespace MiniSpace.Services.Students.Infrastructure.Services +{ + public class DeviceInfoService : IDeviceInfoService + { + public DeviceInfo GetDeviceInfo(HttpContext httpContext) + { + var ipAddress = httpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; + + var userAgent = httpContext.Request.Headers["User-Agent"].ToString().ToLower(); + var deviceType = userAgent.Contains("mobile") ? "Mobile" : "Computer"; + var operatingSystem = userAgent.Contains("windows") ? "Windows" : + userAgent.Contains("mac") ? "MacOS" : + userAgent.Contains("android") ? "Android" : + userAgent.Contains("iphone") ? "iOS" : + userAgent.Contains("linux") ? "Linux" : "Unknown"; + + return new DeviceInfo + { + IpAddress = ipAddress, + DeviceType = deviceType, + OperatingSystem = operatingSystem + }; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/App.razor b/MiniSpace.Web/src/MiniSpace.Web/App.razor index 079b7b5d4..fa3864aa3 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/App.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/App.razor @@ -1,39 +1,7 @@ -@* @using Microsoft.AspNetCore.Components.Authorization -@using Radzen -@using MiniSpace.Web.Areas.Students -@inherits LayoutComponentBase -@inject IIdentityService IdentityService -@inject IStudentsService StudentsService -@inject NavigationManager NavigationManager -@inject Blazored.LocalStorage.ILocalStorageService localStorage -@inject CustomAuthenticationStateProvider CustomAuthenticationStateProvider - - - - - - - - - -

Sorry, there's nothing at this address.

-
-
-
-
- -@code { - protected override async Task OnInitializedAsync() - { - await CustomAuthenticationStateProvider.InitializeAsync(); - await base.OnInitializedAsync(); - } - -} *@ - - -@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Authorization @using MudBlazor.Services +@using MudBlazor +@* *@ diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommandsDto/AddLikeDto.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommandsDto/AddLikeDto.cs new file mode 100644 index 000000000..2a4b53f41 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommandsDto/AddLikeDto.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Web.Areas.Comments.CommandDto +{ + public class AddLikeDto + { + public Guid CommentId { get; } + public Guid UserId { get; } + public string CommentContext { get; } + + public AddLikeDto(Guid commentId, Guid userId, string commentContext) + { + CommentId = commentId; + UserId = userId; + CommentContext = commentContext; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommandsDto/CreateCommentCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommandsDto/CreateCommentCommand.cs new file mode 100644 index 000000000..c6b436e65 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommandsDto/CreateCommentCommand.cs @@ -0,0 +1,25 @@ +using System; + +namespace MiniSpace.Web.Areas.Comments.CommandsDto +{ + public class CreateCommentCommand + { + public Guid CommentId { get; set; } + public Guid ContextId { get; set; } + public string CommentContext { get; set; } + // CommentsContext := UserPost || UserEvent || OrganizationPost || OrganizationEvent + public Guid UserId { get; set; } + public Guid ParentId { get; set; } + public string TextContent { get; set; } + + public CreateCommentCommand(Guid commentId, Guid contextId, string commentContext, Guid userId, Guid parentId, string textContent) + { + CommentId = commentId == Guid.Empty ? Guid.NewGuid() : commentId; + ContextId = contextId; + CommentContext = commentContext; + UserId = userId; + ParentId = parentId; + TextContent = textContent; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommandsDto/SearchRootCommentsCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommandsDto/SearchRootCommentsCommand.cs new file mode 100644 index 000000000..749f38395 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommandsDto/SearchRootCommentsCommand.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MiniSpace.Web.DTO.Wrappers; + +namespace MiniSpace.Web.Areas.Comments.CommandsDto +{ + public class SearchRootCommentsCommand + { + public Guid ContextId { get; set; } + public string CommentContext { get; set; } + public PageableDto Pageable { get; set; } + + public SearchRootCommentsCommand(Guid contextId, string commentContext, PageableDto pageable) + { + ContextId = contextId; + CommentContext = commentContext; + Pageable = pageable; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommandsDto/SearchSubCommentsCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommandsDto/SearchSubCommentsCommand.cs new file mode 100644 index 000000000..a66d76474 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommandsDto/SearchSubCommentsCommand.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MiniSpace.Web.DTO.Wrappers; + +namespace MiniSpace.Web.Areas.Comments.CommandsDto +{ + public class SearchSubCommentsCommand + { + public Guid ContextId { get; set; } + public string CommentContext { get; set; } + public Guid ParentId { get; set; } + public PageableDto Pageable { get; set; } + + public SearchSubCommentsCommand(Guid contextId, string commentContext, Guid parentId, PageableDto pageable) + { + ContextId = contextId; + CommentContext = commentContext; + ParentId = parentId; + Pageable = pageable; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommandsDto/UpdateCommentCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommandsDto/UpdateCommentCommand.cs new file mode 100644 index 000000000..984c5b4bb --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommandsDto/UpdateCommentCommand.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Web.Areas.Comments.CommandsDto +{ + public class UpdateCommentCommand + { + public Guid CommentId { get; set; } + public string TextContent { get; set; } + + public UpdateCommentCommand(Guid commentId, string textContent) + { + CommentId = commentId; + TextContent = textContent; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommentsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommentsService.cs index d95ab120c..9dc6d078e 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommentsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/CommentsService.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using System.Web; +using MiniSpace.Web.Areas.Comments.CommandDto; +using MiniSpace.Web.Areas.Comments.CommandsDto; using MiniSpace.Web.Areas.Identity; -using MiniSpace.Web.DTO; -using MiniSpace.Web.Data.Comments; +using MiniSpace.Web.DTO.Comments; using MiniSpace.Web.DTO.Wrappers; using MiniSpace.Web.HttpClients; @@ -20,18 +23,47 @@ public CommentsService(IHttpClient httpClient, IIdentityService identityService) _identityService = identityService; } - public Task>>> SearchRootCommentsAsync(Guid contextId, - string commentContext, PageableDto pageable) + public Task> SearchRootCommentsAsync(SearchRootCommentsCommand command) { - return _httpClient.PostAsync>>("comments/search", - new (contextId, commentContext, pageable)); + var queryString = ToQueryString(command); + + // Log the query string to the console + Console.WriteLine($"Sending request with query string: comments/search{queryString}"); + + return _httpClient.GetAsync>($"comments/search{queryString}"); } - public Task>>> SearchSubCommentsAsync(Guid contextId, - string commentContext, Guid parentId, PageableDto pageable) + + private string ToQueryString(SearchRootCommentsCommand command) + { + var query = HttpUtility.ParseQueryString(string.Empty); + query["ContextId"] = command.ContextId.ToString(); + query["CommentContext"] = command.CommentContext; + + // Flatten the PageableDto into individual query parameters + if (command.Pageable != null) + { + query["Page"] = command.Pageable.Page.ToString(); + query["Size"] = command.Pageable.Size.ToString(); + + if (command.Pageable.Sort != null) + { + // Pass SortBy as a comma-separated list + if (command.Pageable.Sort.SortBy != null && command.Pageable.Sort.SortBy.Any()) + { + query["SortBy"] = string.Join(",", command.Pageable.Sort.SortBy); + } + query["Direction"] = command.Pageable.Sort.Direction; + } + } + + return "?" + query.ToString(); + } + + + public Task>> SearchSubCommentsAsync(SearchSubCommentsCommand command) { - return _httpClient.PostAsync>>("comments/search", - new (contextId, commentContext, parentId, pageable)); + return _httpClient.PostAsync>("comments/search", command); } public Task GetCommentAsync(Guid commentId) @@ -39,18 +71,16 @@ public Task GetCommentAsync(Guid commentId) return _httpClient.GetAsync($"comments/{commentId}"); } - public Task> CreateCommentAsync(Guid commentId, Guid contextId, string commentContext, - Guid studentId, Guid parentId, string comment) + public Task> CreateCommentAsync(CreateCommentCommand command) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.PostAsync("comments", - new { commentId, contextId, commentContext, studentId, parentId, comment }); + return _httpClient.PostAsync("comments", command); } - public Task> UpdateCommentAsync(Guid commentId, string textContent) + public Task> UpdateCommentAsync(UpdateCommentCommand command) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.PutAsync($"comments/{commentId}", new { commentId, textContent}); + return _httpClient.PutAsync($"comments/{command.CommentId}", command); } public Task DeleteCommentAsync(Guid commentId) @@ -59,10 +89,10 @@ public Task DeleteCommentAsync(Guid commentId) return _httpClient.DeleteAsync($"comments/{commentId}"); } - public Task AddLikeAsync(Guid commentId) + public Task> AddLikeAsync(AddLikeDto command) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.PostAsync($"comments/{commentId}/like", new { commentId }); + return _httpClient.PostAsync($"comments/{command.CommentId}/like", command); } public Task DeleteLikeAsync(Guid commentId) diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/ICommentsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/ICommentsService.cs index 39c3ca3b4..cf5079101 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/ICommentsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Comments/ICommentsService.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using MiniSpace.Web.DTO; -using MiniSpace.Web.DTO.Enums; +using MiniSpace.Web.Areas.Comments.CommandDto; +using MiniSpace.Web.Areas.Comments.CommandsDto; +using MiniSpace.Web.DTO.Comments; using MiniSpace.Web.DTO.Wrappers; using MiniSpace.Web.HttpClients; @@ -10,16 +11,13 @@ namespace MiniSpace.Web.Areas.Comments { public interface ICommentsService { - Task>>> SearchRootCommentsAsync(Guid contextId, - string commentContext, PageableDto pageable); - Task>>> SearchSubCommentsAsync(Guid contextId, - string commentContext, Guid parentId, PageableDto pageable); + Task> SearchRootCommentsAsync(SearchRootCommentsCommand command); + Task>> SearchSubCommentsAsync(SearchSubCommentsCommand command); Task GetCommentAsync(Guid commentId); - Task> CreateCommentAsync(Guid commentId, Guid contextId, string commentContext, - Guid studentId, Guid parentId, string comment); - Task> UpdateCommentAsync(Guid commentId, string textContext); + Task> CreateCommentAsync(CreateCommentCommand command); + Task> UpdateCommentAsync(UpdateCommentCommand command); Task DeleteCommentAsync(Guid commentId); - Task AddLikeAsync(Guid commentId); + Task> AddLikeAsync(AddLikeDto command); Task DeleteLikeAsync(Guid commentId); - } + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/ChatSignalRService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/ChatSignalRService.cs new file mode 100644 index 000000000..56fcb33d8 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/ChatSignalRService.cs @@ -0,0 +1,179 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.JSInterop; +using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.DTO.Communication; +using System; +using System.Threading.Tasks; + +namespace MiniSpace.Web.Areas.Communication +{ + public class ChatSignalRService : IAsyncDisposable + { + private HubConnection _hubConnection; + private readonly NavigationManager _navigationManager; + private readonly IIdentityService _identityService; + private Guid _userId; + private Guid _currentChatId; + private bool _disposed; + + public event Action MessageReceived; + public event Action MessageStatusUpdated; + public event Action TypingNotificationReceived; + public event Action ConnectionChanged; + + public ChatSignalRService(NavigationManager navigationManager, IIdentityService identityService) + { + _navigationManager = navigationManager; + _identityService = identityService; + } + + public async Task StartAsync(Guid userId, Guid currentChatId) + { + if (_hubConnection != null && _hubConnection.State == HubConnectionState.Connected) + { + await _hubConnection.StopAsync(); + } + + _userId = userId; + _currentChatId = currentChatId; + var hubUrl = $"http://localhost:5016/chatHub?userId={userId}&chatId={currentChatId}"; + + _hubConnection = new HubConnectionBuilder() + .WithUrl(hubUrl, options => + { + options.AccessTokenProvider = async () => + { + if (_disposed) + { + return null; + } + + try + { + return await _identityService.GetAccessTokenAsync(); + } + catch (JSDisconnectedException) + { + return null; + } + }; + }) + .WithAutomaticReconnect() + .Build(); + + RegisterHubEvents(); + await StartConnectionAsync(); + } + + public async Task StartAsync(Guid userId) + { + if (_hubConnection != null && _hubConnection.State == HubConnectionState.Connected) + { + await _hubConnection.StopAsync(); + } + + _userId = userId; + _currentChatId = Guid.Empty; + var hubUrl = $"http://localhost:5016/chatHub?userId={userId}"; + + _hubConnection = new HubConnectionBuilder() + .WithUrl(hubUrl, options => + { + options.AccessTokenProvider = async () => + { + if (_disposed) + { + return null; + } + + try + { + return await _identityService.GetAccessTokenAsync(); + } + catch (JSDisconnectedException) + { + return null; + } + }; + }) + .WithAutomaticReconnect() + .Build(); + + RegisterHubEvents(); + await StartConnectionAsync(); + } + + private void RegisterHubEvents() + { + _hubConnection.On("ReceiveMessage", (jsonMessage) => + { + var message = System.Text.Json.JsonSerializer.Deserialize(jsonMessage); + MessageReceived?.Invoke(message); + }); + + _hubConnection.On("ReceiveMessageStatusUpdate", (jsonStatusUpdate) => + { + var statusUpdate = System.Text.Json.JsonSerializer.Deserialize(jsonStatusUpdate); + MessageStatusUpdated?.Invoke(Guid.Parse(statusUpdate.MessageId), statusUpdate.Status); + }); + + _hubConnection.On("ReceiveTypingNotification", (userId, isTyping) => + { + TypingNotificationReceived?.Invoke(userId, isTyping); + }); + + _hubConnection.Reconnecting += (error) => + { + ConnectionChanged?.Invoke(false); + return Task.CompletedTask; + }; + + _hubConnection.Reconnected += (connectionId) => + { + ConnectionChanged?.Invoke(true); + return Task.CompletedTask; + }; + + _hubConnection.Closed += (error) => + { + ConnectionChanged?.Invoke(false); + return Task.CompletedTask; + }; + } + + private async Task StartConnectionAsync() + { + if (!_disposed) + { + await _hubConnection.StartAsync(); + ConnectionChanged?.Invoke(true); + } + } + + public async Task StopAsync() + { + if (_hubConnection != null && _hubConnection.State != HubConnectionState.Disconnected) + { + await _hubConnection.StopAsync(); + } + } + + public async Task SendTypingNotificationAsync(bool isTyping) + { + if (_hubConnection.State == HubConnectionState.Connected) + { + await _hubConnection.InvokeAsync("SendTypingNotification", _currentChatId.ToString(), _userId.ToString(), isTyping); + } + } + + public async ValueTask DisposeAsync() + { + if (_hubConnection != null) + { + await _hubConnection.DisposeAsync(); + } + _disposed = true; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/AddUserToChatCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/AddUserToChatCommand.cs new file mode 100644 index 000000000..398416ae8 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/AddUserToChatCommand.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Web.Areas.Communication.CommandsDto +{ + public class AddUserToChatCommand + { + public Guid ChatId { get; set; } + public Guid UserId { get; set; } + + public AddUserToChatCommand(Guid chatId, Guid userId) + { + ChatId = chatId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/CreateChatCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/CreateChatCommand.cs new file mode 100644 index 000000000..8e4c0ef4c --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/CreateChatCommand.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.Areas.Communication.CommandsDto +{ + public class CreateChatCommand + { + public Guid ChatId { get; set; } + public List ParticipantIds { get; set; } + public string ChatName { get; set; } + + public CreateChatCommand(Guid chatId, List participantIds, string chatName = null) + { + ChatId = chatId; + ParticipantIds = participantIds ?? new List(); + ChatName = chatName; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/DeleteChatCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/DeleteChatCommand.cs new file mode 100644 index 000000000..2ed0047c3 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/DeleteChatCommand.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Web.Areas.Communication.CommandsDto +{ + public class DeleteChatCommand + { + public Guid ChatId { get; set; } + public Guid UserId { get; set; } + + public DeleteChatCommand(Guid chatId, Guid userId) + { + ChatId = chatId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/DeleteMessageCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/DeleteMessageCommand.cs new file mode 100644 index 000000000..855244213 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/DeleteMessageCommand.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Web.Areas.Communication.CommandsDto +{ + public class DeleteMessageCommand + { + public Guid MessageId { get; set; } + public Guid ChatId { get; set; } + + public DeleteMessageCommand(Guid messageId, Guid chatId) + { + MessageId = messageId; + ChatId = chatId; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/RemoveUserFromChatCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/RemoveUserFromChatCommand.cs new file mode 100644 index 000000000..efe064eab --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/RemoveUserFromChatCommand.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Web.Areas.Communication.CommandsDto +{ + public class RemoveUserFromChatCommand + { + public Guid ChatId { get; set; } + public Guid UserId { get; set; } + + public RemoveUserFromChatCommand(Guid chatId, Guid userId) + { + ChatId = chatId; + UserId = userId; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/SendMessageCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/SendMessageCommand.cs new file mode 100644 index 000000000..c6f842ec1 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/SendMessageCommand.cs @@ -0,0 +1,20 @@ +using System; + +namespace MiniSpace.Web.Areas.Communication.CommandsDto +{ + public class SendMessageCommand + { + public Guid ChatId { get; set; } + public Guid SenderId { get; set; } + public string Content { get; set; } + public string MessageType { get; set; } + + public SendMessageCommand(Guid chatId, Guid senderId, string content, string messageType = "Text") + { + ChatId = chatId; + SenderId = senderId; + Content = content; + MessageType = messageType; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/UpdateMessageStatusCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/UpdateMessageStatusCommand.cs new file mode 100644 index 000000000..5f71e6836 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommandsDto/UpdateMessageStatusCommand.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Web.Areas.Communication.CommandsDto +{ + public class UpdateMessageStatusCommand + { + public Guid ChatId { get; set; } + public Guid MessageId { get; set; } + public string Status { get; set; } + + public UpdateMessageStatusCommand(Guid chatId, Guid messageId, string status) + { + ChatId = chatId; + MessageId = messageId; + Status = status; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommunicationService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommunicationService.cs new file mode 100644 index 000000000..8e53cbfad --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/CommunicationService.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Web.DTO; +using MiniSpace.Web.HttpClients; +using MiniSpace.Web.DTO.Wrappers; +using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.DTO.Communication; +using MiniSpace.Web.Areas.Communication.CommandsDto; +using System.Linq; + +namespace MiniSpace.Web.Areas.Communication +{ + public class CommunicationService : ICommunicationService + { + private readonly IHttpClient _httpClient; + private readonly IIdentityService _identityService; + + public CommunicationService(IHttpClient httpClient, IIdentityService identityService) + { + _httpClient = httpClient; + _identityService = identityService; + } + + public async Task> GetUserChatsAsync(Guid userId, int page, int pageSize) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return await _httpClient.GetAsync>($"communication/chats/user/{userId}?page={page}&pageSize={pageSize}"); + } + + public async Task FindExistingChatAsync(Guid userId, Guid friendId) + { + var userChatsResponse = await GetUserChatsAsync(userId, 1, 100); + + if (userChatsResponse == null || !userChatsResponse.Items.Any()) + return null; + + // Loop through all the chats to find one with the friend + foreach (var userChat in userChatsResponse.Items.SelectMany(u => u.Chats)) + { + if (userChat.ParticipantIds.Contains(friendId)) + { + // Return the chat if a matching participant is found + return userChat; + } + } + + // Return null if no existing chat is found + return null; + } + + + public async Task GetChatByIdAsync(Guid chatId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return await _httpClient.GetAsync($"communication/chats/{chatId}"); + } + + public async Task> GetMessagesForChatAsync(Guid chatId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return await _httpClient.GetAsync>($"communication/chats/{chatId}/messages"); + } + + public async Task> CreateChatAsync(CreateChatCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return await _httpClient.PostAsync("communication/chats", command); + } + + public async Task AddUserToChatAsync(Guid chatId, Guid userId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + await _httpClient.PutAsync($"communication/chats/{chatId}/users", new { chatId, userId }); + } + + public async Task DeleteChatAsync(Guid chatId, Guid userId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + var command = new DeleteChatCommand(chatId, userId); + await _httpClient.DeleteAsync($"communication/chats/{chatId}/{userId}", command); + } + + + public async Task> SendMessageAsync(SendMessageCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return await _httpClient.PostAsync($"communication/chats/{command.ChatId}/messages", command); + } + + public async Task> UpdateMessageStatusAsync(UpdateMessageStatusCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return await _httpClient.PutAsync($"communication/chats/{command.ChatId}/messages/{command.MessageId}/status", command); + } + + public async Task DeleteMessageAsync(Guid chatId, Guid messageId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + await _httpClient.DeleteAsync($"communication/chats/{chatId}/messages/{messageId}"); + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/ICommunicationService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/ICommunicationService.cs new file mode 100644 index 000000000..4ce6fdd7d --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Communication/ICommunicationService.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MiniSpace.Web.Areas.Communication.CommandsDto; +using MiniSpace.Web.DTO; +using MiniSpace.Web.DTO.Communication; +using MiniSpace.Web.DTO.Wrappers; +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.Communication +{ + public interface ICommunicationService + { + Task> GetUserChatsAsync(Guid userId, int page, int pageSize); + Task FindExistingChatAsync(Guid userId, Guid friendId); + Task GetChatByIdAsync(Guid chatId); + Task> GetMessagesForChatAsync(Guid chatId); + Task> CreateChatAsync(CreateChatCommand command); + Task AddUserToChatAsync(Guid chatId, Guid userId); + Task DeleteChatAsync(Guid chatId, Guid userId); + Task> SendMessageAsync(SendMessageCommand command); + Task> UpdateMessageStatusAsync(UpdateMessageStatusCommand command); + Task DeleteMessageAsync(Guid chatId, Guid messageId); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/CommandsDto/ViewEventCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/CommandsDto/ViewEventCommand.cs new file mode 100644 index 000000000..47fa7a84e --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/CommandsDto/ViewEventCommand.cs @@ -0,0 +1,16 @@ +using System; + +namespace MiniSpace.Web.Areas.Events.CommandsDto +{ + public class ViewEventCommand + { + public Guid UserId { get; set; } + public Guid EventId { get; set; } + + public ViewEventCommand(Guid userId, Guid eventId) + { + UserId = userId; + EventId = eventId; + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs index f31120fc6..4d9eed430 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs @@ -83,12 +83,6 @@ public Task GetEventRatingAsync(Guid eventId) return _httpClient.GetAsync($"events/{eventId}/rating"); } - // public Task>>> SearchEventsAsync(SearchEvents command) - // { - // _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - // return _httpClient.PostAsync>>("events/search", command); - // } - public Task> SearchEventsAsync(SearchEvents command) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); @@ -124,7 +118,6 @@ public Task> SearchEventsAsync(SearchEvents command) var queryString = "?" + string.Join("&", queryParams); - // Return the correct type based on your API response return _httpClient.GetAsync>($"events/search{queryString}"); } @@ -136,7 +129,6 @@ public Task>>> SearchOrganizerEve return _httpClient.PostAsync>>("events/search/organizer", command); } - // Implementations for participant-related methods public Task GetEventParticipantsAsync(Guid eventId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); @@ -155,14 +147,14 @@ public Task RemoveEventParticipantAsync(Guid eventId, Guid participantId) return _httpClient.DeleteAsync($"events/{eventId}/participants?participantId={participantId}"); } - public Task> GetPaginatedEventsAsync(int page, int pageSize) + public Task> GetPaginatedEventsAsync(int page, int pageSize) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync>($"events/paginated?page={page}&pageSize={pageSize}"); + return _httpClient.GetAsync>($"events/paginated?page={page}&pageSize={pageSize}"); } public async Task> GetMyEventsAsync(Guid organizerId, int page, int pageSize) { - return await _httpClient.GetAsync>($"events/organizer/{organizerId}/paginated?page={page}&pageSize={pageSize}"); + return await _httpClient.GetAsync>($"events/organizer/{organizerId}/paginated?page={page}&pageSize={pageSize}"); } public async Task> GetUserEventsAsync(Guid userId, int page, int pageSize, string engagementType) @@ -171,5 +163,18 @@ public async Task> GetUserEventsAsync(Guid userId, int pag return await _httpClient.GetAsync>($"events/users/{userId}?engagementType={engagementType}&page={page}&pageSize={pageSize}"); } + public async Task> GetUserEventsFeedAsync(Guid userId, int pageNumber, int pageSize, string sortBy, string direction) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + var queryString = $"?pageNumber={pageNumber}&pageSize={pageSize}&sortBy={HttpUtility.UrlEncode(sortBy)}&direction={HttpUtility.UrlEncode(direction)}"; + return await _httpClient.GetAsync>($"events/users/{userId}/feed{queryString}"); + } + + public Task ViewEventAsync(ViewEventCommand command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync($"events/{command.EventId}/view", command); + } + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs index 4bd880b55..dd96637f9 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs @@ -31,5 +31,7 @@ public interface IEventsService Task> GetPaginatedEventsAsync(int page, int pageSize); Task> GetMyEventsAsync(Guid organizerId, int page, int pageSize); Task> GetUserEventsAsync(Guid userId, int page, int pageSize, string engagementType); + Task> GetUserEventsFeedAsync(Guid userId, int pageNumber, int pageSize, string sortBy, string direction); + Task ViewEventAsync(ViewEventCommand command); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/CommandsDto/AddFriendRequestDto.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/CommandsDto/AddFriendRequestDto.cs new file mode 100644 index 000000000..4002ddb4c --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/CommandsDto/AddFriendRequestDto.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Web.Areas.Friends.CommandsDto +{ + public class AddFriendRequestDto + { + public Guid FriendId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/CommandsDto/FriendRequestActionDto.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/CommandsDto/FriendRequestActionDto.cs new file mode 100644 index 000000000..4271344e2 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/CommandsDto/FriendRequestActionDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Web.Areas.Friends.CommandsDto +{ + public class FriendRequestActionDto + { + public Guid RequestId { get; set; } + public Guid RequesterId { get; set; } + public Guid FriendId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/CommandsDto/WithdrawFriendRequestDto.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/CommandsDto/WithdrawFriendRequestDto.cs new file mode 100644 index 000000000..82ce9863c --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/CommandsDto/WithdrawFriendRequestDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Web.Areas.Friends.CommandsDto +{ + public class WithdrawFriendRequestDto + { + public Guid InviterId { get; set; } + public Guid InviteeId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs index 3729fe1cf..3fe2d6341 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs @@ -3,8 +3,11 @@ using System.Linq; using System.Threading.Tasks; using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.Areas.Friends.CommandsDto; +using MiniSpace.Web.Areas.PagedResult; using MiniSpace.Web.DTO; using MiniSpace.Web.HttpClients; +using MiniSpace.Web.DTO.Friends; namespace MiniSpace.Web.Areas.Friends { @@ -12,10 +15,9 @@ public class FriendsService : IFriendsService { private readonly IHttpClient _httpClient; private readonly IIdentityService _identityService; - public FriendDto FriendDto { get; private set; } - + public FriendsService(IHttpClient httpClient, IIdentityService identityService) { _httpClient = httpClient; @@ -31,7 +33,7 @@ public void ClearFriendDto() { FriendDto = null; } - + public async Task GetFriendAsync(Guid friendId) { string accessToken = await _identityService.GetAccessTokenAsync(); @@ -39,28 +41,30 @@ public async Task GetFriendAsync(Guid friendId) return await _httpClient.GetAsync($"friends/{friendId}"); } - public async Task> GetAllFriendsAsync(Guid studentId) + public async Task> GetAllFriendsAsync(Guid userId, int page = 1, int pageSize = 10) { string accessToken = await _identityService.GetAccessTokenAsync(); _httpClient.SetAccessToken(accessToken); - var url = $"friends/{studentId}"; - var studentFriends = await _httpClient.GetAsync>(url); + + string url = $"friends/{userId}?page={page}&pageSize={pageSize}"; + var userFriends = await _httpClient.GetAsync>(url); - var allFriends = studentFriends.SelectMany(sf => sf.Friends).ToList(); + var allFriends = userFriends.Items.SelectMany(uf => uf.Friends).ToList(); foreach (var friend in allFriends) { friend.StudentDetails = await GetStudentAsync(friend.FriendId); } - return allFriends; + return new PagedResult(allFriends, userFriends.Page, userFriends.PageSize, userFriends.TotalItems); } public async Task> AddFriendAsync(Guid friendId) { string accessToken = await _identityService.GetAccessTokenAsync(); _httpClient.SetAccessToken(accessToken); - return await _httpClient.PostAsync("friends", new { friendId }); + var payload = new AddFriendRequestDto { FriendId = friendId }; + return await _httpClient.PostAsync("friends", payload); } public async Task RemoveFriendAsync(Guid friendId) @@ -68,19 +72,16 @@ public async Task RemoveFriendAsync(Guid friendId) string accessToken = await _identityService.GetAccessTokenAsync(); _httpClient.SetAccessToken(accessToken); var requesterId = _identityService.GetCurrentUserId(); - // Console.WriteLine($"Requester ID: {requesterId}"); // Log the requester ID if (requesterId == Guid.Empty) { - // Console.WriteLine("Invalid Requester ID: ID is empty."); return; // Optionally handle the case where the requester ID is invalid } - var payload = new { RequesterId = requesterId, FriendId = friendId }; - // Console.WriteLine($"Payload: {payload.RequesterId}, {payload.FriendId}"); await _httpClient.DeleteAsync($"friends/{requesterId}/{friendId}/remove"); } + // New method to get all students without pagination or filters public async Task> GetAllStudentsAsync() { if (_httpClient == null) throw new InvalidOperationException("HTTP client is not initialized."); @@ -92,12 +93,13 @@ public async Task> GetAllStudentsAsync() return await _httpClient.GetAsync>("students"); } - public async Task> GetAllStudentsAsync(int page = 1, int resultsPerPage = 10, - string searchTerm = null) + // New method to get paginated students with optional search term + public async Task> GetAllStudentsAsync(int page = 1, int pageSize = 10, string searchTerm = null) { string accessToken = await _identityService.GetAccessTokenAsync(); _httpClient.SetAccessToken(accessToken); - string url = $"students?page={page}&resultsPerPage={resultsPerPage}&name={searchTerm}"; + + string url = $"students?page={page}&pageSize={pageSize}&searchTerm={searchTerm}"; return await _httpClient.GetAsync>(url); } @@ -117,149 +119,117 @@ public async Task InviteStudent(Guid inviterId, Guid inviteeId) await _httpClient.PostAsync>($"friends/{inviteeId}/invite", payload); } - // public async Task> GetSentFriendRequestsAsync() - // { - // var studentId = _identityService.GetCurrentUserId(); - // string accessToken = await _identityService.GetAccessTokenAsync(); - // _httpClient.SetAccessToken(accessToken); - // return await _httpClient.GetAsync>($"friends/requests/sent/{studentId}"); - // } - - public async Task GetUserDetails(Guid userId) + public async Task> GetSentFriendRequestsAsync(int page = 1, int pageSize = 10) { + var studentId = _identityService.GetCurrentUserId(); + if (studentId == Guid.Empty) return new PagedResult(Enumerable.Empty(), page, pageSize, 0); + string accessToken = await _identityService.GetAccessTokenAsync(); _httpClient.SetAccessToken(accessToken); - return await _httpClient.GetAsync($"students/{userId}"); - } + string url = $"friends/requests/sent/{studentId}?page={page}&pageSize={pageSize}"; + var studentRequests = await _httpClient.GetAsync>(url); - public async Task> GetSentFriendRequestsAsync() - { - try + if (studentRequests == null || !studentRequests.Items.Any()) { - var studentId = _identityService.GetCurrentUserId(); - if (studentId == Guid.Empty) - { - throw new InvalidOperationException("User ID is not valid."); - } - - string accessToken = await _identityService.GetAccessTokenAsync(); - if (string.IsNullOrEmpty(accessToken)) - { - throw new InvalidOperationException("Access token is missing or invalid."); - } - - _httpClient.SetAccessToken(accessToken); - var studentRequests = await _httpClient.GetAsync>($"friends/requests/sent/{studentId}"); - - if (studentRequests == null || !studentRequests.Any()) - { - return Enumerable.Empty(); - } - - var friendRequests = studentRequests.SelectMany(request => request.FriendRequests).ToList(); - - var inviteeIds = friendRequests.Select(r => r.InviteeId).Distinct(); - var userDetailsTasks = inviteeIds.Select(id => GetUserDetails(id)); - var userDetailsResults = await Task.WhenAll(userDetailsTasks); - - var userDetailsDict = userDetailsResults.ToDictionary(user => user.Id, user => user); - - foreach (var request in friendRequests) - { - if (userDetailsDict.TryGetValue(request.InviteeId, out var userDetails)) - { - request.InviteeName = $"{userDetails.FirstName} {userDetails.LastName}"; - request.InviteeEmail = userDetails.Email; - // request.InviteeImage = userDetails.ProfileImage; // Uncomment if you have a profile image field - } - } - - return friendRequests; + return new PagedResult(Enumerable.Empty(), page, pageSize, 0); } - catch (Exception ex) + + var friendRequests = studentRequests.Items.SelectMany(request => request.FriendRequests).ToList(); + + foreach (var request in friendRequests) { - // Log the exception (optional) - // Console.WriteLine($"Error retrieving sent friend requests: {ex.Message}"); - return new List(); + var userDetails = await GetStudentAsync(request.InviteeId); + request.InviteeName = $"{userDetails.FirstName} {userDetails.LastName}"; + request.InviteeEmail = userDetails.Email; } + + return new PagedResult(friendRequests, studentRequests.Page, studentRequests.PageSize, studentRequests.TotalItems); } - public async Task> GetIncomingFriendRequestsAsync() + public async Task> GetIncomingFriendRequestsAsync(int page = 1, int pageSize = 10) { - try + var userId = _identityService.GetCurrentUserId(); + if (userId == Guid.Empty) return new PagedResult(Enumerable.Empty(), page, pageSize, 0); + + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + string url = $"friends/requests/{userId}?page={page}&pageSize={pageSize}"; + var studentRequests = await _httpClient.GetAsync>(url); + + if (studentRequests == null || !studentRequests.Items.Any()) + { + return new PagedResult(Enumerable.Empty(), page, pageSize, 0); + } + + var incomingRequests = studentRequests.Items.SelectMany(request => request.FriendRequests).ToList(); + + foreach (var request in incomingRequests) { - var userId = _identityService.GetCurrentUserId(); - if (userId == Guid.Empty) - { - throw new InvalidOperationException("User ID is not valid."); - } - - string accessToken = await _identityService.GetAccessTokenAsync(); - _httpClient.SetAccessToken(accessToken); - - var studentRequests = await _httpClient.GetAsync>($"friends/requests/{userId}"); - if (studentRequests == null || !studentRequests.Any()) - { - return Enumerable.Empty(); - } - - var incomingRequests = studentRequests.SelectMany(request => request.FriendRequests).ToList(); - - var inviterIds = incomingRequests.Select(r => r.InviterId).Distinct(); - var userDetailsTasks = inviterIds.Select(id => GetUserDetails(id)); - var userDetailsResults = await Task.WhenAll(userDetailsTasks); - - var userDetailsDict = userDetailsResults.ToDictionary(user => user.Id, user => user); - - foreach (var request in incomingRequests) - { - if (userDetailsDict.TryGetValue(request.InviterId, out var userDetails)) - { - request.InviterName = $"{userDetails.FirstName} {userDetails.LastName}"; - request.InviterEmail = userDetails.Email; - } - } - - return incomingRequests; + var userDetails = await GetStudentAsync(request.InviterId); + request.InviterName = $"{userDetails.FirstName} {userDetails.LastName}"; + request.InviterEmail = userDetails.Email; } - catch (Exception ex) + + return new PagedResult(incomingRequests, studentRequests.Page, studentRequests.PageSize, studentRequests.TotalItems); + } + + public async Task> GetPagedFollowersAsync(Guid userId, int page = 1, int pageSize = 10) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + string url = $"friends/{userId}/followers?page={page}&pageSize={pageSize}"; + var userFollowers = await _httpClient.GetAsync>(url); + + var allFollowers = userFollowers.Items.SelectMany(uf => uf.Friends).ToList(); + + foreach (var follower in allFollowers) { - // Console.WriteLine($"Error retrieving incoming friend requests: {ex.Message}"); - return new List(); + follower.StudentDetails = await GetStudentAsync(follower.UserId); } + + return new PagedResult(allFollowers, userFollowers.Page, userFollowers.PageSize, userFollowers.TotalItems); } + public async Task> GetPagedFollowingAsync(Guid userId, int page = 1, int pageSize = 10) + { + string accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + string url = $"friends/{userId}/following?page={page}&pageSize={pageSize}"; + var userFollowing = await _httpClient.GetAsync>(url); + + var allFollowing = userFollowing.Items.SelectMany(uf => uf.Friends).ToList(); - // private async Task GetUserDetails(Guid userId) - // { - // string accessToken = await _identityService.GetAccessTokenAsync(); - // _httpClient.SetAccessToken(accessToken); - // return await _httpClient.GetAsync($"students/{userId}"); - // } + foreach (var following in allFollowing) + { + following.StudentDetails = await GetStudentAsync(following.FriendId); + } + + return new PagedResult(allFollowing, userFollowing.Page, userFollowing.PageSize, userFollowing.TotalItems); + } - public async Task AcceptFriendRequestAsync(Guid requestId, Guid requesterId, Guid friendId) + public async Task AcceptFriendRequestAsync(FriendRequestActionDto requestAction) { string accessToken = await _identityService.GetAccessTokenAsync(); _httpClient.SetAccessToken(accessToken); - var payload = new { RequesterId = requesterId, FriendId = friendId }; - await _httpClient.PostAsync($"friends/requests/{requestId}/accept", payload); + await _httpClient.PostAsync($"friends/requests/{requestAction.RequestId}/accept", requestAction); } - public async Task DeclineFriendRequestAsync(Guid requestId, Guid requesterId, Guid friendId) + public async Task DeclineFriendRequestAsync(FriendRequestActionDto requestAction) { string accessToken = await _identityService.GetAccessTokenAsync(); _httpClient.SetAccessToken(accessToken); - var payload = new { RequesterId = requesterId, FriendId = friendId }; - await _httpClient.PostAsync($"friends/requests/{requestId}/decline", payload); + await _httpClient.PostAsync($"friends/requests/{requestAction.RequestId}/decline", requestAction); } - public async Task WithdrawFriendRequestAsync(Guid inviterId, Guid inviteeId) + public async Task WithdrawFriendRequestAsync(WithdrawFriendRequestDto withdrawRequest) { string accessToken = await _identityService.GetAccessTokenAsync(); _httpClient.SetAccessToken(accessToken); - var payload = new { InviterId = inviterId, InviteeId = inviteeId }; - await _httpClient.PutAsync($"friends/requests/{inviteeId}/withdraw", payload); + await _httpClient.PutAsync($"friends/requests/{withdrawRequest.InviteeId}/withdraw", withdrawRequest); } - } + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/IFriendsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/IFriendsService.cs index f289bc17b..7ec78463d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/IFriendsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/IFriendsService.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using MiniSpace.Web.Areas.Friends.CommandsDto; +using MiniSpace.Web.Areas.PagedResult; using MiniSpace.Web.DTO; +using MiniSpace.Web.DTO.Friends; using MiniSpace.Web.HttpClients; namespace MiniSpace.Web.Areas.Friends @@ -9,20 +12,39 @@ namespace MiniSpace.Web.Areas.Friends public interface IFriendsService { FriendDto FriendDto { get; } + Task UpdateFriendDto(Guid friendId); + void ClearFriendDto(); + Task GetFriendAsync(Guid friendId); + + Task> GetAllFriendsAsync(Guid userId, int page = 1, int pageSize = 10); + + Task> GetPagedFollowersAsync(Guid userId, int page = 1, int pageSize = 10); + + Task> GetPagedFollowingAsync(Guid userId, int page = 1, int pageSize = 10); + Task> AddFriendAsync(Guid friendId); + Task RemoveFriendAsync(Guid friendId); - Task> GetAllFriendsAsync(Guid studentId); + Task GetStudentAsync(Guid studentId); + Task> GetAllStudentsAsync(); - Task> GetAllStudentsAsync(int page = 1, int resultsPerPage = 10, string search = null); + + Task> GetAllStudentsAsync(int page = 1, int pageSize = 10, string searchTerm = null); + Task InviteStudent(Guid inviterId, Guid inviteeId); - Task> GetSentFriendRequestsAsync(); - Task> GetIncomingFriendRequestsAsync(); - Task AcceptFriendRequestAsync(Guid requestId, Guid requesterId, Guid friendId); - Task DeclineFriendRequestAsync(Guid requestId, Guid requesterId, Guid friendId); - Task WithdrawFriendRequestAsync(Guid inviterId, Guid inviteeId); + + Task> GetSentFriendRequestsAsync(int page = 1, int pageSize = 10); + + Task> GetIncomingFriendRequestsAsync(int page = 1, int pageSize = 10); + + Task AcceptFriendRequestAsync(FriendRequestActionDto requestAction); + + Task DeclineFriendRequestAsync(FriendRequestActionDto requestAction); + + Task WithdrawFriendRequestAsync(WithdrawFriendRequestDto withdrawRequest); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/PagedResult.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/PagedResult.cs deleted file mode 100644 index 7672f5b5b..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/PagedResult.cs +++ /dev/null @@ -1,13 +0,0 @@ - -using System.Collections; -using System.Collections.Generic; - -namespace MiniSpace.Web.Areas.Friends -{ - public class PagedResult - { - public IEnumerable Data { get; set; } - public int TotalCount { get; set; } - public PagedResult() { } - } -} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs index 3938c5a29..136368d23 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IIdentityService.cs @@ -13,7 +13,7 @@ public interface IIdentityService bool IsAuthenticated { get; set; } Task GetAccountAsync(JwtDto jwtDto); Task> SignUpAsync(string firstName, string lastName, string email, string password, string role = "user", IEnumerable permissions = null); - Task> SignInAsync(string email, string password); + Task> SignInAsync(string email, string password, string deviceType); Task Logout(); Task GetAccessTokenAsync(); Task InitializeAuthenticationState(); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs index 6e4ee255f..34d77e961 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityComponent.cs @@ -21,8 +21,8 @@ public Task OnInit() public Task SignUpAsync(string firstName, string lastName, string email, string password, string role = "user") => _identityService.SignUpAsync(firstName, lastName, email, password, role); - public Task> SignInAsync(string email, string password) - => _identityService.SignInAsync(email, password); + public Task> SignInAsync(string email, string password, string deviceType) + => _identityService.SignInAsync(email, password, deviceType); public Task GetAccount(JwtDto jwtDto) => _identityService.GetAccountAsync(jwtDto); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs index 924b1cf72..eae45317d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs @@ -48,9 +48,11 @@ public async Task> SignUpAsync(string firstName, string las new { firstName, lastName, email, password, role, permissions }); } - public async Task> SignInAsync(string email, string password) + public async Task> SignInAsync(string email, string password, string deviceType) { - var response = await _httpClient.PostAsync("identity/sign-in", new { email, password }); + var response = await _httpClient.PostAsync("identity/sign-in", + new { email, password, deviceType }); + if (response.Content != null) { JwtDto = response.Content; @@ -78,6 +80,7 @@ public async Task> SignInAsync(string email, string passwor return response; } + public async Task Logout() { if (JwtDto != null && !string.IsNullOrEmpty(JwtDto.RefreshToken)) diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/INotificationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/INotificationsService.cs index d0836be89..9dc57534d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/INotificationsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/INotificationsService.cs @@ -17,5 +17,6 @@ public interface INotificationsService Task DeleteNotificationAsync(Guid userId, Guid notificationId); Task GetNotificationByIdAsync(Guid userId, Guid notificationId); Task CreateNotificationAsync(NotificationToUsersDto notification); + Task IsUserConnectedAsync(Guid userId); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/NotificationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/NotificationsService.cs index 5e88341b7..9e0820d5b 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/NotificationsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Notifications/NotificationsService.cs @@ -8,6 +8,7 @@ using MiniSpace.Web.HttpClients; using Blazorise; using MiniSpace.Web.DTO.Notifications; +using System.Collections.Concurrent; namespace MiniSpace.Web.Areas.Notifications { @@ -16,6 +17,9 @@ public class NotificationsService: INotificationsService private readonly IHttpClient _httpClient; private readonly IIdentityService _identityService; + private static readonly ConcurrentDictionary ConnectedUsers = new(); + + public NotificationsService(IHttpClient httpClient, IIdentityService identityService) { _httpClient = httpClient; @@ -93,5 +97,22 @@ public async Task CreateNotificationAsync(NotificationToUsersDto notification) await _httpClient.PostAsync>(url, notification); } + public void AddConnectedUser(Guid userId) + { + ConnectedUsers[userId] = true; + } + + // Method to remove a user from the connected users list + public void RemoveConnectedUser(Guid userId) + { + ConnectedUsers.TryRemove(userId, out _); + } + + // Method to check if a user is connected + public Task IsUserConnectedAsync(Guid userId) + { + return Task.FromResult(ConnectedUsers.ContainsKey(userId)); + } + } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs index 32f2e402d..097369fed 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/IOrganizationsService.cs @@ -28,12 +28,13 @@ public interface IOrganizationsService Task SetOrganizationVisibilityAsync(Guid organizationId, SetOrganizationVisibilityCommand command); Task ManageFeedAsync(Guid organizationId, ManageFeedCommand command); Task> UpdateOrganizationAsync(Guid organizationId, UpdateOrganizationCommand command); - Task> GetUserOrganizationsAsync(Guid userId); + Task> GetPaginatedUserOrganizationsAsync(Guid userId, int page, int pageSize); Task> GetOrganizationRolesAsync(Guid organizationId); Task> GetPaginatedOrganizationsAsync(int page, int pageSize, string search = null); Task FollowOrganizationAsync(Guid organizationId); Task AcceptFollowRequestAsync(Guid organizationId, Guid requestId); Task RejectFollowRequestAsync(Guid organizationId, Guid requestId, string reason); Task> GetUserFollowedOrganizationsAsync(Guid userId); + Task> GetOrganizationRequestsAsync(Guid organizationId, int page, int pageSize); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs index 8e1b9ca48..80bcf6b3d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Organizations/OrganizationsService.cs @@ -130,10 +130,11 @@ public Task> UpdateOrganizationAsync(Guid organizationId, U return _httpClient.PutAsync($"organizations/{organizationId}", command); } - public Task> GetUserOrganizationsAsync(Guid userId) + public async Task> GetPaginatedUserOrganizationsAsync(Guid userId, int page, int pageSize) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync>($"organizations/users/{userId}/organizations"); + var queryString = $"organizations/users/{userId}/organizations?page={page}&pageSize={pageSize}"; + return await _httpClient.GetAsync>(queryString); } public Task> GetOrganizationRolesAsync(Guid organizationId) @@ -186,7 +187,14 @@ public Task RejectFollowRequestAsync(Guid organizationId, Guid requestId, string public Task> GetUserFollowedOrganizationsAsync(Guid userId) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.GetAsync>($"users/{userId}/organizations/follow"); + return _httpClient.GetAsync>($"organizations/users/{userId}/organizations/follow"); + } + + public async Task> GetOrganizationRequestsAsync(Guid organizationId, int page, int pageSize) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + var queryString = $"organizations/{organizationId}/requests?page={page}&pageSize={pageSize}"; + return await _httpClient.GetAsync>(queryString); } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs index 523e941cb..b4e0853e4 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs @@ -15,6 +15,7 @@ public interface IPostsService Task ChangePostStateAsync(Guid postId, string state, DateTime publishDate); Task> CreatePostAsync(CreatePostCommand command); Task>> SearchPostsAsync(SearchPosts searchPosts); + Task>> GetUserFeedAsync(Guid userId, int pageNumber, int pageSize, string sortBy = "PublishDate", string direction = "asc"); Task DeletePostAsync(Guid postId); Task> GetPostsAsync(Guid eventId); Task> UpdatePostAsync(Guid postId, string textContent, IEnumerable mediaFiles); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs index 56ca89505..c7d3050df 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs @@ -43,41 +43,69 @@ public Task> CreatePostAsync(CreatePostCommand command) } public async Task>> SearchPostsAsync(SearchPosts searchPosts) -{ - _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - var query = HttpUtility.ParseQueryString(string.Empty); - if (searchPosts.UserId.HasValue) - query["UserId"] = searchPosts.UserId.ToString(); - if (searchPosts.OrganizationId.HasValue) - query["OrganizationId"] = searchPosts.OrganizationId.ToString(); - if (searchPosts.EventId.HasValue) - query["EventId"] = searchPosts.EventId.ToString(); + var query = HttpUtility.ParseQueryString(string.Empty); + if (searchPosts.UserId.HasValue) + query["UserId"] = searchPosts.UserId.ToString(); + if (searchPosts.OrganizationId.HasValue) + query["OrganizationId"] = searchPosts.OrganizationId.ToString(); + if (searchPosts.EventId.HasValue) + query["EventId"] = searchPosts.EventId.ToString(); - query["PageNumber"] = searchPosts.Pageable.Page.ToString(); - query["PageSize"] = searchPosts.Pageable.Size.ToString(); - if (searchPosts.Pageable.Sort?.SortBy != null) - query["SortBy"] = string.Join(",", searchPosts.Pageable.Sort.SortBy); - query["Direction"] = searchPosts.Pageable.Sort?.Direction; + query["PageNumber"] = searchPosts.Pageable.Page.ToString(); + query["PageSize"] = searchPosts.Pageable.Size.ToString(); + if (searchPosts.Pageable.Sort?.SortBy != null) + query["SortBy"] = string.Join(",", searchPosts.Pageable.Sort.SortBy); + query["Direction"] = searchPosts.Pageable.Sort?.Direction; - string queryString = query.ToString(); - string url = $"posts/search?{queryString}"; + string queryString = query.ToString(); + string url = $"posts/search?{queryString}"; - try - { - var result = await _httpClient.GetAsync>(url); - return new HttpResponse>(result); - } - catch (Exception ex) - { - return new HttpResponse>(new ErrorMessage + try + { + var result = await _httpClient.GetAsync>(url); + return new HttpResponse>(result); + } + catch (Exception ex) + { + return new HttpResponse>(new ErrorMessage + { + Code = ex.Message, + Reason = ex.Message + }); + } + } + + public async Task>> GetUserFeedAsync(Guid userId, int pageNumber, + int pageSize, string sortBy = "PublishDate", string direction = "asc") { - Code = ex.Message, - Reason = ex.Message - }); - } -} + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + var query = HttpUtility.ParseQueryString(string.Empty); + query["PageNumber"] = pageNumber.ToString(); + query["PageSize"] = pageSize.ToString(); + query["SortBy"] = sortBy; + query["Direction"] = direction; + + string queryString = query.ToString(); + string url = $"posts/users/{userId}/feed?{queryString}"; + + try + { + var result = await _httpClient.GetAsync>(url); + return new HttpResponse>(result); + } + catch (Exception ex) + { + return new HttpResponse>(new ErrorMessage + { + Code = ex.Message, + Reason = ex.Message + }); + } + } public Task DeletePostAsync(Guid postId) diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/CommandDto/CreateReactionDto.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/CommandDto/CreateReactionDto.cs index ba41277ec..eb1a35892 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/CommandDto/CreateReactionDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/CommandDto/CreateReactionDto.cs @@ -7,14 +7,14 @@ namespace MiniSpace.Web.Areas.Reactions.CommandDto { public class CreateReactionDto { - public Guid ReactionId { get; set;} - public Guid UserId { get; } - public string ReactionType { get; } + public Guid ReactionId { get; set; } = Guid.NewGuid(); + public Guid UserId { get; set; } + public string ReactionType { get; set; } // ReactionType := LoveIt || LikeIt || Wow || ItWasOkay || HateIt - public Guid ContentId { get; } - public string ContentType { get; } + public Guid ContentId { get; set; } + public string ContentType { get; set; } // ContentType := Post || Event || Comment - public string TargetType { get; } + public string TargetType { get; set; } // ReactionTargetType := User || Organization } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/CommandDto/UpdateReactionDto.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/CommandDto/UpdateReactionDto.cs new file mode 100644 index 000000000..b82e17785 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/CommandDto/UpdateReactionDto.cs @@ -0,0 +1,13 @@ +using System; + +namespace MiniSpace.Web.Areas.Reactions.CommandDto +{ + public class UpdateReactionDto + { + public Guid ReactionId { get; set; } + public Guid UserId { get; set; } + public string NewReactionType { get; set; } + public string ContentType { get; set; } + public string TargetType { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/IReactionsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/IReactionsService.cs index 9a51138b6..d646de2ad 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/IReactionsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/IReactionsService.cs @@ -13,6 +13,7 @@ public interface IReactionsService Task> GetReactionsAsync(Guid contentId, ReactionContentType contentType); Task GetReactionsSummaryAsync(Guid contentId, ReactionContentType contentType); Task> CreateReactionAsync(CreateReactionDto command); + Task> UpdateReactionAsync(UpdateReactionDto command); Task DeleteReactionAsync(Guid reactionId); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/ReactionsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/ReactionsService.cs index b6f86e0b7..73aed86fa 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/ReactionsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Reactions/ReactionsService.cs @@ -37,6 +37,13 @@ public Task> CreateReactionAsync(CreateReactionDto command) return _httpClient.PostAsync("reactions", command); } + public Task> UpdateReactionAsync(UpdateReactionDto command) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PutAsync($"reactions/{command.ReactionId}", command); + } + + public Task DeleteReactionAsync(Guid reactionId) { diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/CommandsDto/ViewUserProfileCommand.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/CommandsDto/ViewUserProfileCommand.cs new file mode 100644 index 000000000..a7f9b7d55 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/CommandsDto/ViewUserProfileCommand.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Web.Areas.Students.CommandsDto +{ + public class ViewUserProfileCommand + { + public Guid UserId { get; } + public Guid UserProfileId { get; } + + public ViewUserProfileCommand(Guid userId, Guid userProfileId) + { + UserId = userId; + UserProfileId = userProfileId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs index cf5105ca8..d8c1c2d18 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs @@ -4,6 +4,9 @@ using MiniSpace.Web.DTO; using MiniSpace.Web.DTO.Interests; using MiniSpace.Web.DTO.Languages; +using MiniSpace.Web.DTO.Users; +using MiniSpace.Web.DTO.Views; +using MiniSpace.Web.DTO.Wrappers; using MiniSpace.Web.HttpClients; namespace MiniSpace.Web.Areas.Students @@ -48,5 +51,13 @@ Task UpdateStudentLanguagesAndInterestsAsync( IEnumerable languages, IEnumerable interests); + Task IsUserOnlineAsync(Guid studentId); + + Task ViewUserProfileAsync(Guid userId, Guid userProfileId); + + Task> GetUserProfileViewsAsync(Guid userId, int pageNumber, int pageSize); + Task BlockUserAsync(Guid blockerId, Guid blockedUserId); + Task UnblockUserAsync(Guid blockerId, Guid blockedUserId); + Task> GetBlockedUsersAsync(Guid blockerId, int page, int resultsPerPage); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs index 7f963a115..4a59ddcc8 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs @@ -5,9 +5,14 @@ using System.Text.Json; using System.Threading.Tasks; using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.Areas.Notifications; +using MiniSpace.Web.Areas.Students.CommandsDto; using MiniSpace.Web.DTO; using MiniSpace.Web.DTO.Interests; using MiniSpace.Web.DTO.Languages; +using MiniSpace.Web.DTO.Users; +using MiniSpace.Web.DTO.Views; +using MiniSpace.Web.DTO.Wrappers; using MiniSpace.Web.HttpClients; namespace MiniSpace.Web.Areas.Students @@ -17,6 +22,8 @@ public class StudentsService : IStudentsService private readonly IHttpClient _httpClient; private readonly IIdentityService _identityService; + private readonly INotificationsService _notificationsService; + public StudentDto StudentDto { get; private set; } public StudentsService(IHttpClient httpClient, IIdentityService identityService) @@ -203,5 +210,55 @@ public async Task UpdateStudentLanguagesAndInterestsAsync( await _httpClient.PutAsync($"students/{studentId}/languages-and-interests", updateData); } + + public async Task IsUserOnlineAsync(Guid studentId) + { + return await _notificationsService.IsUserConnectedAsync(studentId); + } + + public async Task ViewUserProfileAsync(Guid userId, Guid userProfileId) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + var command = new ViewUserProfileCommand(userId, userProfileId); + await _httpClient.PostAsync("students/profiles/users/{userProfileId}/view", command); + } + + public async Task> GetUserProfileViewsAsync(Guid userId, int pageNumber, int pageSize) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + var queryString = $"?pageNumber={pageNumber}&pageSize={pageSize}"; + return await _httpClient.GetAsync>($"students/profiles/users/{userId}/views/paginated{queryString}"); + } + + public async Task BlockUserAsync(Guid blockerId, Guid blockedUserId) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + var command = new { blockerId, blockedUserId }; + await _httpClient.PostAsync($"students/{blockerId}/block-user/{blockedUserId}", command); + } + + public async Task UnblockUserAsync(Guid blockerId, Guid blockedUserId) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + var command = new { blockerId, blockedUserId }; + await _httpClient.PostAsync($"students/{blockerId}/unblock-user/{blockedUserId}", command); + } + + public async Task> GetBlockedUsersAsync(Guid blockerId, int page, int resultsPerPage) + { + var accessToken = await _identityService.GetAccessTokenAsync(); + _httpClient.SetAccessToken(accessToken); + + var queryString = $"?page={page}&resultsPerPage={resultsPerPage}"; + return await _httpClient.GetAsync>($"students/{blockerId}/blocked-users{queryString}"); + } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Comments/CommentContext.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Comments/CommentContext.cs new file mode 100644 index 000000000..c6c786038 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Comments/CommentContext.cs @@ -0,0 +1,10 @@ +namespace MiniSpace.Web.DTO.Comments +{ + public enum CommentContext + { + UserPost, + UserEvent, + OrganizationPost, + OrganizationEvent + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/CommentDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Comments/CommentDto.cs similarity index 77% rename from MiniSpace.Web/src/MiniSpace.Web/DTO/CommentDto.cs rename to MiniSpace.Web/src/MiniSpace.Web/DTO/Comments/CommentDto.cs index fde257932..aa2b5f380 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/CommentDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Comments/CommentDto.cs @@ -1,16 +1,15 @@ using System; using System.Collections.Generic; -namespace MiniSpace.Web.DTO +namespace MiniSpace.Web.DTO.Comments { - public class CommentDto + public class CommentDto { public Guid Id { get; set; } public Guid ContextId { get; set; } public string CommentContext { get; set; } - public Guid StudentId { get; set; } - public string StudentName { get; set; } - public HashSet Likes { get; set; } + public Guid UserId { get; set; } + public IEnumerable Likes { get; set; } public Guid ParentId { get; set; } public string TextContent { get; set; } public DateTime CreatedAt { get; set; } @@ -18,10 +17,14 @@ public class CommentDto public DateTime LastReplyAt { get; set; } public int RepliesCount { get; set; } public bool IsDeleted { get; set; } + public IEnumerable Replies { get; set; } + public bool CanExpand { get; set; } public HashSet SubComments { get; set; } public int SubCommentsPage { get; set; } public CommentDto Parent { get; set; } public bool IsLast { get; set; } + } + } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Comments/CommentExtensions.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Comments/CommentExtensions.cs new file mode 100644 index 000000000..e04ebf7f2 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Comments/CommentExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Web.DTO.Comments +{ + public static class CommentExtensions + { + public static ReplyDto ToReplyDto(this CommentDto comment) + { + if (comment == null) + throw new ArgumentNullException(nameof(comment)); + + return new ReplyDto + { + Id = comment.Id, + ParentId = comment.ParentId, + UserId = comment.UserId, + TextContent = comment.TextContent, + CreatedAt = comment.CreatedAt, + IsDeleted = comment.IsDeleted + }; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Comments/ReplyDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Comments/ReplyDto.cs new file mode 100644 index 000000000..1c03d8893 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Comments/ReplyDto.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Web.DTO.Comments +{ + public class ReplyDto + { + public Guid Id { get; set; } + public Guid ParentId { get; set; } + public Guid UserId { get; set; } + public string TextContent { get; set; } + public DateTime CreatedAt { get; set; } + public bool IsDeleted { get; set; } + + public ReplyDto() + { + } + + public ReplyDto(ReplyDto reply) + { + Id = reply.Id; + ParentId = reply.ParentId; + UserId = reply.UserId; + TextContent = reply.TextContent; + CreatedAt = reply.CreatedAt; + IsDeleted = reply.IsDeleted; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/ChatDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/ChatDto.cs new file mode 100644 index 000000000..5a267c24a --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/ChatDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO.Communication +{ + public class ChatDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public List ParticipantIds { get; set; } + public List Messages { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/MessageDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/MessageDto.cs new file mode 100644 index 000000000..a5dac8538 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/MessageDto.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO.Communication +{ + public class MessageDto + { + public Guid Id { get; set; } + public Guid SenderId { get; set; } + public string Content { get; set; } + public Guid ChatId { get; set; } + public DateTime Timestamp { get; set; } + public string MessageType { get; set; } + public string Status { get; set; } + } + +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/MessageStatusUpdateDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/MessageStatusUpdateDto.cs new file mode 100644 index 000000000..4cb68892b --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/MessageStatusUpdateDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO.Communication +{ + public class MessageStatusUpdateDto + { + public string ChatId { get; set; } + public string MessageId { get; set; } + public string Status { get; set; } + } + +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/UserChatDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/UserChatDto.cs new file mode 100644 index 000000000..a7cefacac --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Communication/UserChatDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO.Communication +{ + public class UserChatDto + { + public Guid UserId { get; set; } + public List Chats { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/ReactionType.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/ReactionType.cs index 7d84623a1..b5bc82a4d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/ReactionType.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/ReactionType.cs @@ -20,7 +20,7 @@ public static class ReactionTypeExtensions private const string Hate =@"emoticon-confused"; - public static string GetReactionText(ReactionType? reactionType) + public static string GetReactionText(this ReactionType reactionType) { return reactionType switch { @@ -32,6 +32,7 @@ public static string GetReactionText(ReactionType? reactionType) _ => "No reactions!" }; } + public static string GetReactionIcon(this ReactionType? reactionType) { diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/Reactions/ReactionTargetType.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/Reactions/ReactionTargetType.cs new file mode 100644 index 000000000..47547d464 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Enums/Reactions/ReactionTargetType.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Web.DTO.Enums.Reactions +{ + public enum ReactionTargetType + { + User, + Organization + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/FriendDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Friends/FriendDto.cs similarity index 52% rename from MiniSpace.Web/src/MiniSpace.Web/DTO/FriendDto.cs rename to MiniSpace.Web/src/MiniSpace.Web/DTO/Friends/FriendDto.cs index e00cb739a..9b9854a20 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/FriendDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Friends/FriendDto.cs @@ -2,20 +2,15 @@ using System.Collections.Generic; using MiniSpace.Web.DTO.States; -namespace MiniSpace.Web.DTO +namespace MiniSpace.Web.DTO.Friends { public class FriendDto { public Guid Id { get; set; } - public string Email { get; set; } - public string FirstName { get; set; } - public Guid StudentId { get; set; } + public Guid UserId { get; set; } public Guid FriendId { get; set; } - public string LastName { get; set; } - public string FullName => $"{FirstName} {LastName}"; public DateTime CreatedAt { get; set; } public FriendState State { get; set; } - public string ProfileImage { get; set; } public StudentDto StudentDetails { get; set; } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/FriendRequestDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Friends/FriendRequestDto.cs similarity index 88% rename from MiniSpace.Web/src/MiniSpace.Web/DTO/FriendRequestDto.cs rename to MiniSpace.Web/src/MiniSpace.Web/DTO/Friends/FriendRequestDto.cs index 2ed375678..43e5dc8c8 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/FriendRequestDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Friends/FriendRequestDto.cs @@ -1,7 +1,7 @@ using System; using MiniSpace.Web.DTO.States; -namespace MiniSpace.Web.DTO +namespace MiniSpace.Web.DTO.Friends { public class FriendRequestDto { @@ -10,7 +10,7 @@ public class FriendRequestDto public Guid InviteeId { get; set; } public DateTime RequestedAt { get; set; } public FriendState State { get; set; } - public Guid StudentId { get; set; } + public Guid UserId { get; set; } public string InviteeName { get; set; } public string InviterName { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentFriendsDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Friends/UserFriendsDto.cs similarity index 63% rename from MiniSpace.Web/src/MiniSpace.Web/DTO/StudentFriendsDto.cs rename to MiniSpace.Web/src/MiniSpace.Web/DTO/Friends/UserFriendsDto.cs index f12f8d300..96bf80b8b 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentFriendsDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Friends/UserFriendsDto.cs @@ -3,11 +3,11 @@ using System.Linq; using System.Threading.Tasks; -namespace MiniSpace.Web.DTO +namespace MiniSpace.Web.DTO.Friends { - public class StudentFriendsDto + public class UserFriendsDto { - public Guid StudentId { get; set; } + public Guid UserId { get; set; } public List Friends { get; set; } = new List(); } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentRequestsDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Friends/UserRequestsDto.cs similarity index 64% rename from MiniSpace.Web/src/MiniSpace.Web/DTO/StudentRequestsDto.cs rename to MiniSpace.Web/src/MiniSpace.Web/DTO/Friends/UserRequestsDto.cs index 542198ded..2c54142d8 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentRequestsDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Friends/UserRequestsDto.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; -namespace MiniSpace.Web.DTO +namespace MiniSpace.Web.DTO.Friends { - public class StudentRequestsDto + public class UserRequestsDto { public Guid Id { get; set; } - public Guid StudentId { get; set; } + public Guid UserId { get; set; } public List FriendRequests { get; set; } = new List(); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationDto.cs index 95f3b98da..b52139fb6 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationDto.cs @@ -23,6 +23,10 @@ public class OrganizationDto public string Email { get; set; } public IEnumerable Users { get; set; } = new List(); + public OrganizationSettingsDto Settings { get; set; } = new OrganizationSettingsDto(); public int UserCount => Users?.Count() ?? 0; + + public bool IsExpanded { get; set; } = false; + public List SubOrganizations { get; set; } = new List(); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationRequestDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationRequestDto.cs new file mode 100644 index 000000000..471511760 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationRequestDto.cs @@ -0,0 +1,13 @@ +using System; + +namespace MiniSpace.Web.DTO.Organizations +{ + public class OrganizationRequestDto + { + public Guid RequestId { get; set; } + public Guid UserId { get; set; } + public DateTime RequestDate { get; set; } + public string State { get; set; } + public string Reason { get; set; } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationRequestsDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationRequestsDto.cs new file mode 100644 index 000000000..10bedfd2e --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Organizations/OrganizationRequestsDto.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace MiniSpace.Web.DTO.Organizations +{ + public class OrganizationRequestsDto + { + public Guid OrganizationId { get; set; } + public IEnumerable Requests { get; set; } + + public OrganizationRequestsDto() + { + Requests = new List(); + } + + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/ReactionDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/ReactionDto.cs index 910681383..a006a88cd 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/ReactionDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/ReactionDto.cs @@ -1,15 +1,16 @@ using System; using MiniSpace.Web.DTO.Enums; +using MiniSpace.Web.DTO.Enums.Reactions; namespace MiniSpace.Web.DTO { public class ReactionDto { public Guid Id { get; set; } - public Guid StudentId { get; set; } - public string StudentFullName { get; set; } + public Guid UserId { get; set; } public Guid ContentId { get; set; } public ReactionContentType ContentType { get; set; } - public ReactionType Type { get; set; } + public ReactionType ReactionType { get; set; } + public ReactionTargetType TargetType { get; set; } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/ReactionsSummaryDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/ReactionsSummaryDto.cs index 6e3cfbef3..ea827b8e1 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/ReactionsSummaryDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/ReactionsSummaryDto.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using MiniSpace.Web.DTO.Enums; namespace MiniSpace.Web.DTO @@ -9,5 +10,6 @@ public class ReactionsSummaryDto public ReactionType? DominantReaction { get; set; } public Guid? AuthUserReactionId { get; set; } public ReactionType? AuthUserReactionType { get; set; } + public Dictionary ReactionsWithCounts { get; set; } = new Dictionary(); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Users/BlockedUserDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Users/BlockedUserDto.cs new file mode 100644 index 000000000..d12c24c53 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Users/BlockedUserDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Web.DTO.Users +{ + public class BlockedUserDto + { + public Guid BlockerId { get; set; } + public Guid BlockedUserId { get; set; } + public DateTime BlockedAt { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Views/UserProfileViewDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Views/UserProfileViewDto.cs new file mode 100644 index 000000000..7ec2ae91e --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Views/UserProfileViewDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Web.DTO.Views +{ + public class UserProfileViewDto + { + public Guid UserProfileId { get; set; } + public DateTime Date { get; set; } + public string IpAddress { get; set; } + public string DeviceType { get; set; } + public string OperatingSystem { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Wrappers/PagedResponseDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Wrappers/PagedResponseDto.cs index 0c16846a1..06f4ec9d1 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/Wrappers/PagedResponseDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Wrappers/PagedResponseDto.cs @@ -5,20 +5,16 @@ namespace MiniSpace.Web.DTO.Wrappers { public class PagedResponseDto : ResponseDto { - public IEnumerable Items { get; } - public int TotalPages { get; } - public int TotalItems { get; } - public int PageSize { get; } - public int Page { get; } - public bool First { get; } - public bool Last { get; } - public bool Empty { get; } + public IEnumerable Items { get; set; } = new List(); + public int TotalPages { get; set; } + public int TotalItems { get; set; } + public int PageSize { get; set; } + public int Page { get; set; } + public bool First { get; set; } + public bool Last { get; set; } + public bool Empty { get; set; } public int? NextPage => Page < TotalPages ? Page + 1 : (int?)null; public int? PreviousPage => Page > 1 ? Page - 1 : (int?)null; - - public PagedResponseDto() - { - Items = new List(); - } } + } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/BlockedUsers/BlockedUserViewModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/BlockedUsers/BlockedUserViewModel.cs new file mode 100644 index 000000000..59072dd4a --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/BlockedUsers/BlockedUserViewModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MiniSpace.Web.Models.BlockedUsers +{ + public class BlockedUserViewModel + { + public Guid BlockedUserId { get; set; } + public string FullName { get; set; } + public string ProfileImageUrl { get; set; } + public DateTime BlockedAt { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/BlockedListComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/BlockedListComponent.razor new file mode 100644 index 000000000..5b605b16d --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/BlockedListComponent.razor @@ -0,0 +1,145 @@ +@page "/blocked-users" +@inject IStudentsService StudentsService +@inject IIdentityService IdentityService +@inject ISnackbar Snackbar +@inject NavigationManager NavigationManager +@using MiniSpace.Web.DTO.Users +@using MiniSpace.Web.Models.BlockedUsers +@using MudBlazor +@using System.Collections.Generic +@using System.Threading.Tasks + + + + Blocked Users + @if (isLoading) + { + + } + else + { + @if (blockedUsers?.Any() == true) + { + @foreach (var user in blockedUsers) + { + + + + + + + + + + + @user.FullName + Blocked on: @user.BlockedAt.ToString("MMMM dd, yyyy") + + + + + +
+ + Unblock + +
+
+
+
+
+ } + } + else + { + You have no blocked users. + } + } +
+ + + +@code { + private List blockedUsers = new(); + private bool isLoading = true; + + protected override async Task OnInitializedAsync() + { + isLoading = true; + await LoadBlockedUsersAsync(); + isLoading = false; + } + + private async Task LoadBlockedUsersAsync() + { + try + { + var currentUserId = IdentityService.GetCurrentUserId(); + var response = await StudentsService.GetBlockedUsersAsync(currentUserId, 1, 10); + var blockedUserIds = response.Items.Select(bu => bu.BlockedUserId).ToList(); + + foreach (var blockedUserId in blockedUserIds) + { + var student = await StudentsService.GetStudentAsync(blockedUserId); + if (student != null) + { + blockedUsers.Add(new BlockedUserViewModel + { + BlockedUserId = blockedUserId, + FullName = $"{student.FirstName} {student.LastName}", + ProfileImageUrl = GetProfileImageUrl(student.ProfileImageUrl), + BlockedAt = response.Items.First(bu => bu.BlockedUserId == blockedUserId).BlockedAt + }); + } + } + } + catch (Exception ex) + { + Snackbar.Add($"Error loading blocked users: {ex.Message}", Severity.Error); + } + } + + private async Task UnblockUser(Guid blockedUserId) + { + try + { + var currentUserId = IdentityService.GetCurrentUserId(); + await StudentsService.UnblockUserAsync(currentUserId, blockedUserId); + blockedUsers.RemoveAll(u => u.BlockedUserId == blockedUserId); + Snackbar.Add("User has been unblocked successfully.", Severity.Success); + StateHasChanged(); + } + catch (Exception ex) + { + Snackbar.Add($"Error unblocking user: {ex.Message}", Severity.Error); + } + } + + private string GetProfileImageUrl(string profileImageUrl) + { + return string.IsNullOrEmpty(profileImageUrl) ? "images/default_profile_image.webp" : profileImageUrl; + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ProfileComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ProfileComponent.razor index 4c3ba0bc7..5a3fb082a 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ProfileComponent.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ProfileComponent.razor @@ -129,7 +129,7 @@ else } - + Add Education @@ -169,7 +169,7 @@ else } - + Add Work Experience diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ShowAccount.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ShowAccount.razor index 5eb22dbb2..535a4c8e6 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ShowAccount.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ShowAccount.razor @@ -30,6 +30,7 @@ Languages & Interests Gallery User Settings + Blocked Users @@ -92,6 +93,10 @@ { } + else if (activeTabIndex == 7) + { + + } @@ -157,7 +162,7 @@ @code { private List _items = new List { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), new BreadcrumbItem("Account settings", href: "/events/follow", disabled: true, icon: @Icons.Material.Filled.ManageAccounts), }; diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignIn.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignIn.razor index 4a60ec66c..b71b282fa 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignIn.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/SignIn.razor @@ -11,12 +11,10 @@ @inject NavigationManager NavigationManager @inject IJSRuntime JSRuntime - + +@code { + [Parameter] public Guid ChatId { get; set; } + + private List userChats = new(); + private List messages = new(); + private string newMessageContent = string.Empty; + private Dictionary userNames = new(); + private Dictionary userImages = new(); + private Dictionary lastMessages = new(); + private Dictionary typingStatus = new(); + private bool isSending = false; + private bool hasUpdatedStatus = false; + private bool isUserTyping = false; + private string typingUserName = string.Empty; + private Timer typingTimer; + private bool isConnected = false; // Start as not connected + private bool IsSendButtonDisabledCombined => IsSendButtonDisabled || !isConnected; + + private bool IsSendButtonDisabled => isSending || string.IsNullOrWhiteSpace(newMessageContent); + + protected override async Task OnInitializedAsync() + { + await IdentityService.InitializeAuthenticationState(); + + if (IdentityService.IsAuthenticated) + { + var userId = IdentityService.GetCurrentUserId(); + ChatSignalRService.ConnectionChanged += OnConnectionChanged; + ChatSignalRService.MessageReceived += OnMessageReceived; + ChatSignalRService.MessageStatusUpdated += OnMessageStatusUpdated; + ChatSignalRService.TypingNotificationReceived += OnTypingNotificationReceived; + + await InitializeSignalRConnection(userId); + + if (isConnected) + { + await LoadUserChats(); + if (ChatId != Guid.Empty) + { + await LoadMessages(ChatId); + } + } + else + { + NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); + } + } + else + { + NavigationManager.NavigateTo("/login"); + } + } + + protected override async Task OnParametersSetAsync() + { + if (isConnected) + { + await LoadMessages(ChatId); + } + } + + private async Task InitializeSignalRConnection(Guid userId) + { + if (!isConnected) + { + await ChatSignalRService.StartAsync(userId, ChatId); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await ScrollToBottomAsync(); + } + else + { + await JSRuntime.InvokeVoidAsync("scrollToBottom", "chatMessagesContainer"); + } + + if (!hasUpdatedStatus) + { + hasUpdatedStatus = true; + await Task.Delay(1000); + await UpdateUnreadMessagesStatusAsync(); + } + } + + private async void OnTypingTimeout(object state) + { + await InvokeAsync(() => + { + if (state is Guid chatId) + { + typingStatus[chatId] = false; + isUserTyping = false; + typingUserName = string.Empty; + StateHasChanged(); + } + }); + + typingTimer?.Dispose(); + } + + private async Task HandleInputChange(ChangeEventArgs e) + { + if (e.Value is string inputValue) + { + await ChatSignalRService.SendTypingNotificationAsync(!string.IsNullOrEmpty(inputValue)); + + typingTimer?.Dispose(); + typingTimer = new Timer(OnTypingTimeout, ChatId, 1000, Timeout.Infinite); + } + } + + private async void OnTypingNotificationReceived(string userId, bool isTyping) + { + await InvokeAsync(() => + { + var chatId = ChatId; + + if (userNames.TryGetValue(Guid.Parse(userId), out var userName)) + { + typingUserName = userName; + } + else + { + typingUserName = "Unknown User"; + } + + isUserTyping = isTyping; + typingStatus[chatId] = isTyping; + + if (isTyping) + { + typingTimer?.Dispose(); + typingTimer = new Timer(OnTypingTimeout, chatId, 1000, Timeout.Infinite); + } + + StateHasChanged(); + }); + } + + private async void OnConnectionChanged(bool connected) + { + await InvokeAsync(async () => + { + isConnected = connected; + if (isConnected) + { + await LoadUserChats(); + if (ChatId != Guid.Empty) + { + await LoadMessages(ChatId); + } + StateHasChanged(); + } + }); + } + + private async Task LoadUserChats() + { + try + { + var userId = IdentityService.GetCurrentUserId(); + var result = await CommunicationService.GetUserChatsAsync(userId, 1, 20); + + if (result != null) + { + userChats = result.Items.SelectMany(u => u.Chats).ToList(); + await LoadUserDetails(); + await LoadLastMessages(); + } + } + catch (Exception ex) + { + Snackbar.Add($"Failed to load chats: {ex.Message}", Severity.Error); + } + } + + private async Task LoadMessages(Guid chatId) + { + try + { + messages = (await CommunicationService.GetMessagesForChatAsync(chatId)).ToList(); + await LoadUserDetails(); + await ScrollToBottomAsync(); + } + catch (Exception ex) + { + Snackbar.Add($"Failed to load messages: {ex.Message}", Severity.Error); + } + } + + private async Task LoadUserDetails() + { + var senderIds = messages.Select(m => m.SenderId).Distinct().ToList(); + var chatUserIds = userChats.SelectMany(c => c.ParticipantIds).Distinct().ToList(); + var allUserIds = senderIds.Union(chatUserIds).Distinct().ToList(); + + foreach (var userId in allUserIds) + { + if (!userNames.ContainsKey(userId)) + { + var user = await StudentsService.GetStudentAsync(userId); + if (user != null) + { + userNames[userId] = $"{user.FirstName} {user.LastName}"; + userImages[userId] = string.IsNullOrWhiteSpace(user.ProfileImageUrl) ? "images/default_profile_image.webp" : user.ProfileImageUrl; + } + } + } + } + + private async Task LoadLastMessages() + { + foreach (var chat in userChats) + { + var messages = await CommunicationService.GetMessagesForChatAsync(chat.Id); + lastMessages[chat.Id] = messages.OrderByDescending(m => m.Timestamp).FirstOrDefault(); + } + StateHasChanged(); + } + + private async Task SendMessage() + { + if (!string.IsNullOrWhiteSpace(newMessageContent) && !isSending) + { + isSending = true; + + try + { + var userId = IdentityService.GetCurrentUserId(); + var command = new SendMessageCommand(ChatId, userId, newMessageContent); + + var response = await CommunicationService.SendMessageAsync(command); + if (response.IsSuccessStatusCode) + { + newMessageContent = string.Empty; + Snackbar.Add("Message sent!", Severity.Success); + await ScrollToBottomAsync(); + } + else + { + Snackbar.Add("Failed to send message.", Severity.Error); + } + } + catch (Exception ex) + { + Snackbar.Add($"Error: {ex.Message}", Severity.Error); + } + finally + { + isSending = false; + } + } + } + + private async Task ScrollToBottomAsync() + { + await JSRuntime.InvokeVoidAsync("scrollToBottom", "chatMessagesContainer"); + } + + private async Task UpdateUnreadMessagesStatusAsync() + { + var unreadMessages = messages + .Where(m => m.SenderId != IdentityService.GetCurrentUserId() && m.Status != "Read") + .ToList(); + + if (unreadMessages.Any()) + { + foreach (var message in unreadMessages) + { + await UpdateMessageStatus(message, "Read"); + } + } + } + + private async Task UpdateMessageStatus(MessageDto message, string status) + { + if (message.ChatId == Guid.Empty || message.Id == Guid.Empty) + { + Snackbar.Add("Invalid message ID or chat ID.", Severity.Error); + return; + } + + var command = new UpdateMessageStatusCommand(message.ChatId, message.Id, status); + var response = await CommunicationService.UpdateMessageStatusAsync(command); + + if (response.IsSuccessStatusCode) + { + message.Status = status; + } + else + { + Snackbar.Add($"Failed to update message status: {response.ErrorMessage}", Severity.Error); + } + } + + private void SelectChat(Guid chatId) + { + NavigationManager.NavigateTo($"/chats/{chatId}"); + } + + private string GetChatItemClass(Guid chatId) + { + return ChatId == chatId ? "selected-chat" : string.Empty; + } + + private int GetUnreadMessageCount(Guid chatId) + { + return messages.Count(m => m.ChatId == chatId && m.SenderId != IdentityService.GetCurrentUserId() && m.Status != "Read"); + } + + private string GetMessageBubbleClass(MessageDto message) + { + return message.SenderId == IdentityService.GetCurrentUserId() ? "sent" : "received"; + } + + private string GetSenderName(Guid senderId) + { + return userNames.TryGetValue(senderId, out var name) ? name : "Unknown"; + } + + private string GetSenderImage(Guid senderId) + { + return userImages.TryGetValue(senderId, out var imageUrl) ? imageUrl : "/images/default_profile_image.webp"; + } + + private string GetChatImage(Guid chatId) + { + if (IdentityService.IsAuthenticated) + { + var chat = userChats.FirstOrDefault(c => c.Id == chatId); + + if (chat == null) + { + return "/images/default_profile_image.webp"; + } + + var userId = IdentityService.GetCurrentUserId(); + if (chat.ParticipantIds.Count == 2) + { + var otherParticipantId = chat.ParticipantIds.FirstOrDefault(id => id != userId); + return GetSenderImage(otherParticipantId); + } + + var otherParticipant = chat.ParticipantIds.FirstOrDefault(id => id != userId); + if (otherParticipant != Guid.Empty) + { + return GetSenderImage(otherParticipant); + } + } + + return "/images/default_profile_image.webp"; + } + + private string GetChatName(Guid chatId) + { + var chat = userChats.FirstOrDefault(c => c.Id == chatId); + + if (IdentityService.IsAuthenticated) + { + var userId = IdentityService.GetCurrentUserId(); + if (chat != null && chat.ParticipantIds.Count == 2) + { + var otherParticipantId = chat.ParticipantIds.FirstOrDefault(id => id != userId); + return userNames.TryGetValue(otherParticipantId, out var otherParticipantName) + ? otherParticipantName + : "Unknown Chat"; + } + + return chat?.Name ?? "Unknown Chat"; + } + return "Unknown Chat"; + } + + private async void OnMessageReceived(MessageDto message) + { + await InvokeAsync(() => + { + if (message.ChatId == ChatId) + { + if (!messages.Any(m => m.Id == message.Id)) + { + messages.Add(message); + ScrollToBottomAsync(); + StateHasChanged(); + } + } + + if (lastMessages.ContainsKey(message.ChatId)) + { + lastMessages[message.ChatId] = message; + } + else + { + lastMessages.Add(message.ChatId, message); + } + + StateHasChanged(); + }); + } + + private async void OnMessageStatusUpdated(Guid messageId, string status) + { + await InvokeAsync(() => + { + var message = messages.FirstOrDefault(m => m.Id == messageId); + if (message != null) + { + message.Status = status; + StateHasChanged(); + } + }); + } + + private string GetLastMessagePreview(Guid chatId) + { + if (lastMessages.TryGetValue(chatId, out var lastMessage)) + { + return $"{GetSenderName(lastMessage.SenderId)}: {lastMessage.Content}"; + } + return "No messages yet"; + } + + private string GetLastMessageTime(Guid chatId) + { + if (lastMessages.TryGetValue(chatId, out var lastMessage)) + { + return lastMessage.Timestamp.ToString("g"); + } + return string.Empty; + } + + private bool IsUserTypingInChat(Guid chatId) + { + return typingStatus.TryGetValue(chatId, out var isTyping) && isTyping; + } + + private string GetStatusIcon(string status) + { + return status switch + { + "Sent" => Icons.Material.Filled.Check, + "Delivered" => Icons.Material.Filled.DoneAll, + "Read" => Icons.Material.Filled.Visibility, + _ => Icons.Material.Filled.Schedule // Default icon for pending or unknown status + }; + } + + public async ValueTask DisposeAsync() + { + ChatSignalRService.MessageReceived -= OnMessageReceived; + ChatSignalRService.MessageStatusUpdated -= OnMessageStatusUpdated; + ChatSignalRService.TypingNotificationReceived -= OnTypingNotificationReceived; + ChatSignalRService.ConnectionChanged -= OnConnectionChanged; + await ChatSignalRService.DisposeAsync(); + typingTimer?.Dispose(); + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/ChatsAll.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/ChatsAll.razor new file mode 100644 index 000000000..0677efc1d --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/ChatsAll.razor @@ -0,0 +1,411 @@ +@page "/chats/all" +@using MiniSpace.Web.HttpClients +@using MiniSpace.Web.Areas.Communication +@using MiniSpace.Web.DTO.Communication +@inject IIdentityService IdentityService +@inject IStudentsService StudentsService +@inject ICommunicationService CommunicationService +@inject NavigationManager NavigationManager +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject ChatSignalRService ChatSignalRService +@using MudBlazor +@using System.Threading +@implements IAsyncDisposable + + + + + + + + @if (filteredChats.Any()) + { + @foreach (var chat in filteredChats) + { + var lastMessage = lastMessages.ContainsKey(chat.Id) ? lastMessages[chat.Id] : null; + var isTyping = typingStatus.ContainsKey(chat.Id); + + + + +
+ @GetChatName(chat.Id) + + @if (isTyping) + { + @typingStatus[chat.Id].UserName is typing... + } + else + { + @GetSenderName(lastMessage?.SenderId ?? Guid.Empty): @lastMessage?.Content + } + +
+ @lastMessage?.Timestamp.ToString("MMM d, h:mm tt") + + Delete Chat + +
+ + } + } + else + { + No chats found. + } +
+
+ + + +@code { + private List userChats = new(); + private Dictionary lastMessages = new(); + private Dictionary userNames = new(); + private Dictionary userImages = new(); + private Dictionary typingStatus = new(); + private string searchQuery = string.Empty; + + private IEnumerable filteredChats => userChats + .Where(chat => string.IsNullOrEmpty(searchQuery) || + GetChatName(chat.Id).Contains(searchQuery, StringComparison.OrdinalIgnoreCase) || + (lastMessages.ContainsKey(chat.Id) && lastMessages[chat.Id]?.Content.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) == true)) + .ToList(); + + protected override async Task OnInitializedAsync() + { + await IdentityService.InitializeAuthenticationState(); + + if (IdentityService.IsAuthenticated) + { + await LoadUserChats(); + + var userId = IdentityService.GetCurrentUserId(); + await ChatSignalRService.StartAsync(userId); + ChatSignalRService.MessageReceived += OnMessageReceived; + ChatSignalRService.TypingNotificationReceived += OnTypingNotificationReceived; + } + else + { + NavigationManager.NavigateTo("/login"); + } + } + + private async Task LoadUserChats() + { + try + { + var userId = IdentityService.GetCurrentUserId(); + var result = await CommunicationService.GetUserChatsAsync(userId, 1, 20); + + if (result != null) + { + userChats = result.Items.SelectMany(u => u.Chats).ToList(); + await LoadLastMessages(); + await LoadUserDetails(); + + userChats = userChats + .OrderByDescending(c => lastMessages.ContainsKey(c.Id) ? lastMessages[c.Id]?.Timestamp : DateTime.MinValue) + .ToList(); + } + } + catch (Exception ex) + { + Snackbar.Add($"Failed to load chats: {ex.Message}", Severity.Error); + } + } + + private async Task LoadLastMessages() + { + foreach (var chat in userChats) + { + var messages = await CommunicationService.GetMessagesForChatAsync(chat.Id); + lastMessages[chat.Id] = messages.OrderByDescending(m => m.Timestamp).FirstOrDefault(); + } + } + + private async Task LoadUserDetails() + { + var allUserIds = userChats.SelectMany(c => c.ParticipantIds).Distinct().ToList(); + + foreach (var userId in allUserIds) + { + if (!userNames.ContainsKey(userId)) + { + var user = await StudentsService.GetStudentAsync(userId); + if (user != null) + { + userNames[userId] = $"{user.FirstName} {user.LastName}"; + userImages[userId] = string.IsNullOrWhiteSpace(user.ProfileImageUrl) ? "images/default_profile_image.webp" : user.ProfileImageUrl; + } + } + } + } + + private void SelectChat(Guid chatId) + { + NavigationManager.NavigateTo($"/chats/{chatId}"); + } + + private async Task ShowDeleteChatDialog(Guid chatId) + { + var parameters = new DialogParameters + { + ["Message"] = "Are you sure you want to delete this chat? This action cannot be undone." + }; + + var options = new DialogOptions + { + CloseOnEscapeKey = true, + MaxWidth = MaxWidth.Small, + CloseButton = true, + DisableBackdropClick = true, + ClassBackground = "mud-dialog-blur-backdrop" + }; + + var dialog = DialogService.Show("Delete Chat", parameters, options); + + var result = await dialog.Result; + + if (!result.Canceled) + { + await DeleteChat(chatId); + } + } + + private async Task DeleteChat(Guid chatId) + { + try + { + var userId = IdentityService.GetCurrentUserId(); + + await CommunicationService.DeleteChatAsync(chatId, userId); + + userChats = userChats.Where(c => c.Id != chatId).ToList(); + Snackbar.Add("Chat deleted successfully.", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"Failed to delete chat: {ex.Message}", Severity.Error); + } + } + + private string GetChatName(Guid chatId) + { + var chat = userChats.FirstOrDefault(c => c.Id == chatId); + + if (chat != null && chat.ParticipantIds.Count == 2) + { + var otherParticipantId = chat.ParticipantIds.FirstOrDefault(id => id != IdentityService.GetCurrentUserId()); + return userNames.TryGetValue(otherParticipantId, out var otherParticipantName) ? otherParticipantName : "Unknown Chat"; + } + + return chat?.Name ?? "Group Chat"; + } + + private string GetChatImage(Guid chatId) + { + var chat = userChats.FirstOrDefault(c => c.Id == chatId); + if (chat == null) return "/images/default_profile_image.webp"; + + var userId = IdentityService.GetCurrentUserId(); + if (chat.ParticipantIds.Count == 2) + { + var otherParticipantId = chat.ParticipantIds.FirstOrDefault(id => id != userId); + return GetSenderImage(otherParticipantId); + } + + return "/images/default_profile_image.webp"; + } + + private string GetSenderImage(Guid senderId) + { + return userImages.TryGetValue(senderId, out var imageUrl) ? imageUrl : "/images/default_profile_image.webp"; + } + + private string GetSenderName(Guid senderId) + { + return userNames.TryGetValue(senderId, out var name) ? name : "Unknown"; + } + + private async void OnMessageReceived(MessageDto message) + { + await InvokeAsync(async () => + { + // Update last message in chat + lastMessages[message.ChatId] = message; + + // Move chat to the top of the list + var chat = userChats.FirstOrDefault(c => c.Id == message.ChatId); + if (chat != null) + { + userChats.Remove(chat); + userChats.Insert(0, chat); + } + else + { + // Load chat if not found (e.g., was deleted locally) + await LoadUserChats(); + } + + StateHasChanged(); + }); + } + + private async void OnTypingNotificationReceived(string userId, bool isTyping) + { + await InvokeAsync(() => + { + var parsedUserId = Guid.Parse(userId); + var chatId = userChats.FirstOrDefault(c => c.ParticipantIds.Contains(parsedUserId))?.Id ?? Guid.Empty; + + if (chatId != Guid.Empty) + { + if (isTyping) + { + if (userNames.TryGetValue(parsedUserId, out var typingUserName)) + { + typingStatus[chatId] = (typingUserName, new Timer(OnTypingTimeout, chatId, 1000, Timeout.Infinite)); + } + } + else + { + if (typingStatus.TryGetValue(chatId, out var typingEntry)) + { + typingEntry.Timer.Dispose(); + typingStatus.Remove(chatId); + } + } + + StateHasChanged(); + } + }); + } + + private void OnTypingTimeout(object state) + { + if (state is Guid chatId && typingStatus.TryGetValue(chatId, out var typingEntry)) + { + typingEntry.Timer.Dispose(); + typingStatus.Remove(chatId); + + InvokeAsync(() => StateHasChanged()); + } + } + + public async ValueTask DisposeAsync() + { + foreach (var (_, timer) in typingStatus.Values) + { + timer.Dispose(); + } + + ChatSignalRService.MessageReceived -= OnMessageReceived; + ChatSignalRService.TypingNotificationReceived -= OnTypingNotificationReceived; + await ChatSignalRService.DisposeAsync(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/DeleteChatDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/DeleteChatDialog.razor new file mode 100644 index 000000000..339f00394 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/DeleteChatDialog.razor @@ -0,0 +1,19 @@ +@using MudBlazor + + + + @Message + + + Cancel + Delete + + + +@code { + [CascadingParameter] MudDialogInstance MudDialog { get; set; } + [Parameter] public string Message { get; set; } + + private void DeleteChat() => MudDialog.Close(DialogResult.Ok(true)); + private void Cancel() => MudDialog.Cancel(); +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/NewChat.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/NewChat.razor new file mode 100644 index 000000000..6b8cc15dd --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Chats/NewChat.razor @@ -0,0 +1,302 @@ +@page "/chats/new" +@using MiniSpace.Web.HttpClients +@using MiniSpace.Web.Areas.Friends +@using MiniSpace.Web.DTO +@inject IIdentityService IdentityService +@inject IFriendsService FriendsService +@inject ICommunicationService CommunicationService +@inject NavigationManager NavigationManager +@inject ISnackbar Snackbar +@using MudBlazor + + + + + +
+
+ + + @if (!pageInitialized) + { +
+ +
+ } + else if (friends != null && friends.Any()) + { + foreach (var friend in friends) + { +
+
+ Friend Image +
+
@friend.StudentDetails.FirstName @friend.StudentDetails.LastName
+

@friend.StudentDetails.Email

+
+
+ + + Start Chat + +
+
+
+ } + + @if (hasMorePages) + { + + + Load More + + } + } + else + { +

No friends to show. Start connecting now!

+ } +
+
+
+
+ + + +@code { + private List friends = new List(); + private string searchTerm; + private bool pageInitialized; + private int currentPage = 1; + private int pageSize = 10; + private int totalFriends; + private bool hasMorePages => friends.Count < totalFriends; + + + private List _items = new List + { + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Chats", href: "/chats", icon: Icons.Material.Filled.Chat), + new BreadcrumbItem("New Chat", href: "/chats/new", disabled: true, icon: Icons.Material.Filled.AddComment), + }; + + protected override async Task OnInitializedAsync() + { + await IdentityService.InitializeAuthenticationState(); + if (IdentityService.IsAuthenticated) + { + await LoadFriends(); + pageInitialized = true; + } + else + { + NavigationManager.NavigateTo("/login"); + } + } + + private string GetProfileImageUrl(string profileImageUrl) + { + return string.IsNullOrEmpty(profileImageUrl) ? "images/default_profile_image.webp" : profileImageUrl; + } + + private async Task StartChatWithFriend(Guid friendId, string friendName) + { + try + { + // Check if the chat already exists + var userId = IdentityService.GetCurrentUserId(); + var existingChat = await CommunicationService.FindExistingChatAsync(userId, friendId); + + if (existingChat != null) + { + // If chat exists, navigate to it + Console.WriteLine("Navigating to existing chat..."); + NavigationManager.NavigateTo($"/chats/{existingChat.Id}"); + } + else + { + // If no chat exists, create a new one + var command = new CreateChatCommand( + chatId: Guid.NewGuid(), + participantIds: new List { userId, friendId }, + chatName: $"Chat with {friendName}" + ); + + var response = await CommunicationService.CreateChatAsync(command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add($"You have started a chat with {friendName}.", Severity.Success); + NavigationManager.NavigateTo($"/chats/{response.Content}"); + } + else + { + Snackbar.Add("An error occurred while creating the chat.", Severity.Error); + } + } + } + catch (Exception ex) + { + Snackbar.Add($"An error occurred: {ex.Message}", Severity.Error); + } + } + + private void SearchFriends() + { + if (!string.IsNullOrWhiteSpace(searchTerm)) + { + searchTerm = searchTerm.Trim(); + friends = friends.Where(f => f.StudentDetails.FirstName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || f.StudentDetails.LastName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)).ToList(); + } + else + { + ReloadFriends().Wait(); + } + StateHasChanged(); + } + + private async Task ReloadFriends() + { + currentPage = 1; + friends.Clear(); + await LoadFriends(); + } + + private async Task LoadFriends() + { + try + { + var studentId = IdentityService.GetCurrentUserId(); + var result = await FriendsService.GetAllFriendsAsync(studentId, currentPage, pageSize); + if (result != null) + { + friends.AddRange(result.Items); + totalFriends = result.TotalItems; + } + } + catch (Exception ex) + { + Snackbar.Add($"Failed to Load Friends: {ex.Message}", Severity.Error); + } + } + + private async Task LoadMoreFriends() + { + currentPage++; + await LoadFriends(); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/MyEvents.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/MyEvents.razor index d0b3d89fe..36f114f46 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/MyEvents.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/MyEvents.razor @@ -89,6 +89,6 @@ private void ViewEvent(Guid eventId) { - NavigationManager.NavigateTo($"/events/{eventId}"); + NavigationManager.NavigateTo($"/events/event/{eventId}"); } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/Home.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/Home.razor index 231331851..e085838bd 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/Home.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/Home.razor @@ -6,173 +6,108 @@ @inject NavigationManager NavigationManager @inject IIdentityService IdentityService @inject IPostsService PostsService +@inject ISnackbar Snackbar @using MudBlazor - - - - - - - - Discover What's New - @if (pageInitialized) - { - @if (posts.Any()) - { - - @foreach (var post in posts) - { - - - - } - - } - else - { - No activity found - Please join an event first - } - } - - - - - - - - -@code { - private IEnumerable posts; - private Guid studentId; - private bool pageInitialized = false; - private SearchPosts searchModel; - - protected override async Task OnInitializedAsync() - { - if (IdentityService != null && IdentityService.IsAuthenticated) + + @if (!pageInitialized) { - studentId = IdentityService.GetCurrentUserId(); - searchModel = InitializeSearchModel(studentId); - var result = await PostsService.SearchPostsAsync(searchModel); - posts = result.Content.Items; - pageInitialized = true; +
+ + Loading, please wait... +
} else { - NavigationManager.NavigateTo(""); - } - } - - private static SearchPosts InitializeSearchModel(Guid studentId) - { - return new() - { - UserId = studentId, - Pageable = new PageableDto() - { - Page = 1, - Size = 8, - Sort = new SortDto() - { - SortBy = new List() { "publishDate" }, - Direction = "desc" - } - } - }; - } -} + + + + + + + + + Discover What's New + @if (postsLoadingFailed) + { + Failed to load posts. Please try again later. + } + else if (posts != null && posts.Any()) + { + + @foreach (var post in posts) + { + + + + } + + } + else + { + No activity found + Please join an event first + } + -@* @page "/home" -@using MiniSpace.Web.DTO -@using MiniSpace.Web.Areas.Posts -@using MiniSpace.Web.Data.Posts -@using MiniSpace.Web.Components -@using MiniSpace.Web.DTO.Wrappers -@using Radzen -@using System.Globalization -@inject NavigationManager NavigationManager -@inject IIdentityService IdentityService -@inject IPostsService PostsService - -

Discover What's New

- -@if (pageInitialized) -{ - @if (posts.Any()) - { - - - - - - } - else - { -

No activity found

- - } -} - - - + + + + +
+ } +
+ @code { private IEnumerable posts; private Guid studentId; private bool pageInitialized = false; - private SearchPosts searchModel; - + private bool postsLoadingFailed = false; + + private List _items = new List + { + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), + @* new BreadcrumbItem("Account settings", href: "/events/follow", disabled: true, icon: @Icons.Material.Filled.ManageAccounts), *@ + }; + protected override async Task OnInitializedAsync() { if (IdentityService != null && IdentityService.IsAuthenticated) { studentId = IdentityService.GetCurrentUserId(); - searchModel = InitializeSearchModel(studentId); - var result = await PostsService.SearchPostsAsync(searchModel); - posts = result.Content.Content; - pageInitialized = true; + try + { + var result = await PostsService.GetUserFeedAsync(studentId, 1, 8, "PublishDate", "desc"); + + if (result.IsSuccessStatusCode) + { + posts = result.Content.Items; + } + else + { + posts = new List(); // Handle error gracefully + postsLoadingFailed = true; + Snackbar.Add($"Error loading posts: {result.ErrorMessage.Reason}", Severity.Error); + } + } + catch (Exception ex) + { + postsLoadingFailed = true; + Snackbar.Add($"Exception occurred: {ex.Message}", Severity.Error); + } + finally + { + pageInitialized = true; + } } else { NavigationManager.NavigateTo(""); - } } - - private static SearchPosts InitializeSearchModel(Guid studentId) - { - return new() - { - StudentId = studentId, - Pageable = new PageableDto() - { - Page = 1, - Size = 8, - Sort = new SortDto() - { - SortBy = new List() {"publishDate"}, - Direction = "des" - } - } - }; - } -} *@ +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/PostCard.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/PostCard.razor index 9a63b514d..bb64e5462 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/PostCard.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/PostCard.razor @@ -1,20 +1,435 @@ @using MiniSpace.Web.DTO @using MudBlazor +@inject IReactionsService ReactionsService +@inject ICommentsService CommentsService +@inject IStudentsService StudentsService +@inject IIdentityService IdentityService +@inject NavigationManager NavigationManager +@using MiniSpace.Web.Areas.Comments.CommandDto - - - - @Post.TextContent - + - @Post.State + + + + + + @GetUserName(Post.UserId) + @Post.CreatedAt.ToString("g") + + + + + + @if (Post.MediaFiles != null && Post.MediaFiles.Any()) + { + + } + + @if (ReactionsSummary != null) + { + + @foreach (var reaction in ReactionsSummary.ReactionsWithCounts.OrderByDescending(r => r.Value)) + { + + + @reaction.Value + + } + + Total: @ReactionsSummary.NumberOfReactions + + + } - - Read More + + + + + + View + + + + + + React + + + + @foreach (var reactionType in Enum.GetValues(typeof(ReactionType)).Cast()) + { + + @reactionType.GetReactionText() + + } + + + + + Comment + + + @if (IsCommentSectionVisible) + { + + + Submit + + @if (Comments != null && Comments.Any()) + { + + @foreach (var comment in Comments) + { + + + + + + + @GetUserName(comment.UserId) + @comment.TextContent + @comment.CreatedAt.ToString("g") + + + @($"{comment.Likes?.Count() ?? 0} people liked this comment") + + + + Like + + + + Reply + + + @if (comment.Id == activeReplyCommentId) + { + + Submit Reply + } + + @if (comment.Replies != null && comment.Replies.Any()) + { + + @foreach (var reply in comment.Replies) + { + + + + + + + @GetUserName(reply.UserId) + @reply.TextContent + @reply.CreatedAt.ToString("g") + + + + + Like + + + + + } + + } + + + + } + + } + else + { + No comments available. + } + + } @code { [Parameter] public PostDto Post { get; set; } + + private ReactionsSummaryDto ReactionsSummary { get; set; } + private List Comments { get; set; } = new(); + private bool IsCommentSectionVisible { get; set; } = false; + private string NewCommentText { get; set; } = string.Empty; + private string newReplyText { get; set; } = string.Empty; + private Guid? activeReplyCommentId { get; set; } = null; + + private Dictionary studentsCache = new(); + + protected override async Task OnInitializedAsync() + { + ReactionsSummary = await ReactionsService.GetReactionsSummaryAsync(Post.Id, ReactionContentType.Post); + Comments = await LoadCommentsForPostAsync(); + + if (Post.UserId.HasValue && !studentsCache.ContainsKey(Post.UserId.Value)) + { + var student = await StudentsService.GetStudentAsync(Post.UserId.Value); + if (student != null) + { + studentsCache[Post.UserId.Value] = student; + } + } + } + + private async Task> LoadCommentsForPostAsync() + { + var command = new SearchRootCommentsCommand( + contextId: Post.Id, + commentContext: DetermineCommentContext().ToString(), + pageable: new PageableDto + { + Page = 1, + Size = 10, + Sort = new SortDto + { + SortBy = new[] { "CreatedAt" }, + Direction = "asc" + } + } + ); + + var response = await CommentsService.SearchRootCommentsAsync(command); + var comments = response.Items?.ToList() ?? new List(); + + foreach (var comment in comments) + { + if (!studentsCache.ContainsKey(comment.UserId)) + { + var student = await StudentsService.GetStudentAsync(comment.UserId); + if (student != null) + { + studentsCache[comment.UserId] = student; + } + } + + if (comment.Replies != null) + { + foreach (var reply in comment.Replies) + { + if (!studentsCache.ContainsKey(reply.UserId)) + { + var replyAuthor = await StudentsService.GetStudentAsync(reply.UserId); + if (replyAuthor != null) + { + studentsCache[reply.UserId] = replyAuthor; + } + } + } + } + } + + return comments; + } + + private CommentContext DetermineCommentContext() + { + if (Post.OrganizationId.HasValue) + { + return Post.EventId.HasValue ? CommentContext.OrganizationEvent : CommentContext.OrganizationPost; + } + else + { + return Post.EventId.HasValue ? CommentContext.UserEvent : CommentContext.UserPost; + } + } + + private void ToggleCommentSection() + { + IsCommentSectionVisible = !IsCommentSectionVisible; + NewCommentText = string.Empty; + } + + private void ToggleReplySection(Guid commentId) + { + if (activeReplyCommentId == commentId) + { + activeReplyCommentId = null; + newReplyText = string.Empty; + } + else + { + activeReplyCommentId = commentId; + newReplyText = string.Empty; + } + } + + private async Task SubmitCommentAsync() + { + if (string.IsNullOrWhiteSpace(NewCommentText)) + { + return; + } + + var commentContext = DetermineCommentContext(); + var userId = IdentityService.GetCurrentUserId(); + + var command = new CreateCommentCommand( + commentId: Guid.NewGuid(), + contextId: Post.Id, + commentContext: commentContext.ToString(), + userId: userId, + parentId: Guid.Empty, + textContent: NewCommentText + ); + + var response = await CommentsService.CreateCommentAsync(command); + + if (response.IsSuccessStatusCode) + { + NewCommentText = string.Empty; + Comments = await LoadCommentsForPostAsync(); + } + else + { + // Handle error (e.g., show a snackbar) + } + } + + private async Task SubmitReplyAsync(CommentDto parentComment) + { + if (string.IsNullOrWhiteSpace(newReplyText)) + { + return; + } + + var commentContext = DetermineCommentContext(); + var userId = IdentityService.GetCurrentUserId(); + + var command = new CreateCommentCommand( + commentId: Guid.NewGuid(), + contextId: Post.Id, + commentContext: commentContext.ToString(), + userId: userId, + parentId: parentComment.Id, + textContent: newReplyText + ); + + var response = await CommentsService.CreateCommentAsync(command); + + if (response.IsSuccessStatusCode) + { + newReplyText = string.Empty; + Comments = await LoadCommentsForPostAsync(); + activeReplyCommentId = null; + } + else + { + // Handle error (e.g., show a snackbar) + } + } + + private async Task HandleReactionAsync(ReactionType reactionType) + { + var reactions = await ReactionsService.GetReactionsAsync(Post.Id, ReactionContentType.Post); + var existingReaction = reactions.FirstOrDefault(r => r.UserId == IdentityService.UserDto.Id); + + if (existingReaction != null) + { + var updateReaction = new UpdateReactionDto + { + ReactionId = existingReaction.Id, + UserId = IdentityService.UserDto.Id, + NewReactionType = reactionType.ToString(), + ContentType = "Post", + TargetType = Post.OrganizationId.HasValue ? "Organization" : "User" + }; + + var updateResult = await ReactionsService.UpdateReactionAsync(updateReaction); + + if (updateResult.IsSuccessStatusCode) + { + // Reaction updated successfully + } + else + { + // Handle error (e.g., show a snackbar) + } + } + else + { + var createReaction = new CreateReactionDto + { + UserId = IdentityService.UserDto.Id, + ContentId = Post.Id, + ContentType = "Post", + ReactionType = reactionType.ToString(), + TargetType = Post.OrganizationId.HasValue ? "Organization" : "User" + }; + + var createResult = await ReactionsService.CreateReactionAsync(createReaction); + + if (createResult.IsSuccessStatusCode) + { + // Reaction added successfully + } + else + { + // Handle error (e.g., show a snackbar) + } + } + + ReactionsSummary = await ReactionsService.GetReactionsSummaryAsync(Post.Id, ReactionContentType.Post); // Refresh reactions summary + } + + private async Task AddLikeToCommentAsync(CommentDto comment) + { + var command = new AddLikeDto(comment.Id, IdentityService.GetCurrentUserId(), DetermineCommentContext().ToString()); + + var response = await CommentsService.AddLikeAsync(command); + + if (response.IsSuccessStatusCode) + { + Comments = await LoadCommentsForPostAsync(); // Refresh comments + } + else + { + // Handle error (e.g., show a snackbar) + } + } + + private async Task AddLikeToReplyAsync(ReplyDto reply) + { + var command = new AddLikeDto(reply.Id, IdentityService.GetCurrentUserId(), DetermineCommentContext().ToString()); + + var response = await CommentsService.AddLikeAsync(command); + + if (response.IsSuccessStatusCode) + { + Comments = await LoadCommentsForPostAsync(); // Refresh comments + } + else + { + // Handle error (e.g., show a snackbar) + } + } + + private string GetUserAvatar(Guid? userId) + { + if (userId.HasValue && studentsCache.ContainsKey(userId.Value)) + { + return studentsCache[userId.Value].ProfileImageUrl ?? string.Empty; + } + return string.Empty; + } + + private string GetUserName(Guid? userId) + { + if (userId.HasValue && studentsCache.ContainsKey(userId.Value)) + { + return $"{studentsCache[userId.Value].FirstName} {studentsCache[userId.Value].LastName}"; + } + return "Unknown User"; + } + + private void NavigateToPostDetails() + { + NavigationManager.NavigateTo($"/posts/details/{Post.Id}"); + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserInformation.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserInformation.razor index ca5567418..843c2f28d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserInformation.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserInformation.razor @@ -1,125 +1,197 @@ @page "/user-information/{UserId:guid}" @using MiniSpace.Web.DTO @using MiniSpace.Web.Areas.Students +@using MiniSpace.Web.Areas.Events @inject IStudentsService StudentsService +@inject IEventsService EventsService @inject NavigationManager NavigationManager +@inject ISnackbar Snackbar @using MudBlazor - @if (student != null) - { - - - - + + + + + + @if (loadingProfile) + { + + } + else if (profileLoadFailed) + { + Failed to load user profile. Please try again later. + } + else if (student != null) + {
- +
@($"{student.FirstName} {student.LastName}") @student.Description - Edit Profile + Edit Profile + Public Profile
+ } +
+
+
+ + + @if (educationLoadFailed) + { + + Failed to load education details. + + } + else if (loadingEducation) + { + + + + } + else if (student?.Education != null && student.Education.Any()) + { + + + + + Education + + + @foreach (var education in student.Education) + { + @education.Degree at @education.InstitutionName + @education.StartDate?.ToString("MMMM yyyy") - @education.EndDate?.ToString("MMMM yyyy") + } + } - @if (!string.IsNullOrEmpty(student.ContactEmail)) - { - - - - - Contact Information - - - Contact Email: @student.ContactEmail - - - - } - - @if (student.Education != null && student.Education.Any()) - { - - - - - Education - - - @foreach (var education in student.Education) - { - Institution: @education.InstitutionName, Degree: @education.Degree, Period: @education.StartDate?.ToString("yyyy-MM-dd") - @education.EndDate?.ToString("yyyy-MM-dd") - } - - - - } - - @if (student.Work != null && student.Work.Any()) - { - - - - - Work Experience - - - @foreach (var work in student.Work) - { - Position: @work.Position, Company: @work.Company, Period: @work.StartDate?.ToString("yyyy-MM-dd") - @work.EndDate?.ToString("yyyy-MM-dd") - } - - - - } + + @if (workLoadFailed) + { + + Failed to load work experience. + + } + else if (loadingWork) + { + + + + } + else if (student?.Work != null && student.Work.Any()) + { + + + + + Work Experience + + + @foreach (var work in student.Work) + { + @work.Position at @work.Company + @work.StartDate?.ToString("MMMM yyyy") - @work.EndDate?.ToString("MMMM yyyy") + } + + + + } - @if ((student.Languages != null && student.Languages.Any()) || (student.Interests != null && student.Interests.Any())) - { - - - - - Skills and Interests - - - @if (student.Languages != null && student.Languages.Any()) - { - Languages: @string.Join(", ", student.Languages.Select(l => l.ToString())) - } - @if (student.Interests != null && student.Interests.Any()) - { - Interests: @string.Join(", ", student.Interests.Select(i => i.ToString())) - } - - - - } + + @if (languagesLoadFailed) + { + + Failed to load languages and interests. + + } + else if (loadingLanguages) + { + + + + } + else if ((student?.Languages != null && student.Languages.Any()) || (student?.Interests != null && student.Interests.Any())) + { + + + + + Skills and Interests + + + @if (student.Languages != null && student.Languages.Any()) + { + + Languages: + @foreach (var language in student.Languages) + { + @language + } + + } + @if (student.Interests != null && student.Interests.Any()) + { + + Interests: + @foreach (var interest in student.Interests) + { + @interest + } + + } + + + + } - @if ((student.InterestedInEvents != null && student.InterestedInEvents.Any()) || (student.SignedUpEvents != null && student.SignedUpEvents.Any())) - { - - - - - Events - - - @if (student.InterestedInEvents != null && student.InterestedInEvents.Any()) + + @if (eventsLoadFailed) + { + + Failed to load events. + + } + else if (loadingEvents) + { + + + + } + else if (InterestedInEventsDetails.Any() || SignedUpEventsDetails.Any()) + { + + + + + Events + + + @if (InterestedInEventsDetails.Any()) + { + Interested in: + @foreach (var eventDetail in InterestedInEventsDetails) { - Interested in Events: @string.Join(", ", student.InterestedInEvents.Select(e => e.ToString())) + @eventDetail.Name - @eventDetail.StartDate.ToString("MMMM dd, yyyy") } - @if (student.SignedUpEvents != null && student.SignedUpEvents.Any()) + } + @if (SignedUpEventsDetails.Any()) + { + Signed Up for: + @foreach (var eventDetail in SignedUpEventsDetails) { - Signed Up for Events: @string.Join(", ", student.SignedUpEvents.Select(e => e.ToString())) + @eventDetail.Name - @eventDetail.StartDate.ToString("MMMM dd, yyyy") } - - - - } -
- } + } +
+
+
+ } +
@code { @@ -127,15 +199,184 @@ public Guid UserId { get; set; } private StudentDto student; + private List InterestedInEventsDetails = new List(); + private List SignedUpEventsDetails = new List(); + + // Individual section loading and error state flags + private bool loadingProfile = true; + private bool profileLoadFailed = false; + + private bool loadingEducation = true; + private bool educationLoadFailed = false; + + private bool loadingWork = true; + private bool workLoadFailed = false; + + private bool loadingLanguages = true; + private bool languagesLoadFailed = false; + + private bool loadingEvents = true; + private bool eventsLoadFailed = false; + + private static readonly Random random = new Random(); protected override async Task OnInitializedAsync() { - student = await StudentsService.GetStudentAsync(UserId); + await LoadProfileAsync(); + + if (!profileLoadFailed) + { + await Task.WhenAll( + LoadEducationAsync(), + LoadWorkExperienceAsync(), + LoadLanguagesAndInterestsAsync(), + LoadEventsAsync() + ); + } + else + { + loadingEducation = false; + loadingWork = false; + loadingLanguages = false; + loadingEvents = false; + } + } + + private async Task LoadProfileAsync() + { + try + { + student = await StudentsService.GetStudentAsync(UserId); + } + catch (Exception ex) + { + profileLoadFailed = true; + Snackbar.Add($"Error loading user profile: {ex.Message}", Severity.Error); + } + finally + { + loadingProfile = false; + } + } + + private async Task LoadEducationAsync() + { + try + { + } + catch (Exception ex) + { + educationLoadFailed = true; + Snackbar.Add($"Error loading education details: {ex.Message}", Severity.Error); + } + finally + { + loadingEducation = false; + } + } + + private async Task LoadWorkExperienceAsync() + { + try + { + } + catch (Exception ex) + { + workLoadFailed = true; + Snackbar.Add($"Error loading work experience: {ex.Message}", Severity.Error); + } + finally + { + loadingWork = false; + } + } + + private async Task LoadLanguagesAndInterestsAsync() + { + try + { + } + catch (Exception ex) + { + languagesLoadFailed = true; + Snackbar.Add($"Error loading languages and interests: {ex.Message}", Severity.Error); + } + finally + { + loadingLanguages = false; + } + } + + private async Task LoadEventsAsync() + { + try + { + if (student.InterestedInEvents != null && student.InterestedInEvents.Any()) + { + foreach (var eventId in student.InterestedInEvents) + { + try + { + var eventDetail = await EventsService.GetEventAsync(eventId); + if (eventDetail != null) + { + InterestedInEventsDetails.Add(eventDetail); + } + else + { + Snackbar.Add($"Event details not found for event ID: {eventId}", Severity.Warning); + } + } + catch (Exception ex) + { + Snackbar.Add($"Error loading event details for event ID {eventId}: {ex.Message}", Severity.Error); + } + } + } + + if (student.SignedUpEvents != null && student.SignedUpEvents.Any()) + { + foreach (var eventId in student.SignedUpEvents) + { + try + { + var eventDetail = await EventsService.GetEventAsync(eventId); + if (eventDetail != null) + { + SignedUpEventsDetails.Add(eventDetail); + } + else + { + Snackbar.Add($"Signed up event details not found for event ID: {eventId}", Severity.Warning); + } + } + catch (Exception ex) + { + Snackbar.Add($"Error loading signed up event for event ID {eventId}: {ex.Message}", Severity.Error); + } + } + } + + if (!InterestedInEventsDetails.Any() && !SignedUpEventsDetails.Any()) + { + eventsLoadFailed = true; + Snackbar.Add("No events found for this user.", Severity.Warning); + } + } + catch (Exception ex) + { + eventsLoadFailed = true; + Snackbar.Add($"Error loading events: {ex.Message}", Severity.Error); + } + finally + { + loadingEvents = false; + } } private string GetProfileImage() { - var defaultImage = "path/to/default/image.png"; // Set path to your default image + var defaultImage = "images/default_profile_image.webp"; var validExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp" }; if (string.IsNullOrEmpty(student?.ProfileImageUrl) || !validExtensions.Contains(System.IO.Path.GetExtension(student.ProfileImageUrl)?.ToLower())) @@ -150,15 +391,29 @@ { NavigationManager.NavigateTo("/account"); } + + private void NavigateToPublicProfile() + { + NavigationManager.NavigateTo($"/user-details/{UserId}"); + } + + + + private int GetRandomFontSize() + { + return random.Next(14, 24); + } } + + + + .text-muted { + color: #7f8c8d; + } + + .skill-interest-text { + word-wrap: break-word; + white-space: normal; + display: flex; + flex-wrap: wrap; + } + + .skill-interest-item { + margin-right: 8px; + margin-bottom: 8px; + background-color: #f5f5f5; + padding: 2px 6px; + border-radius: 4px; + line-height: 1.2; /* Ensures good vertical spacing */ + display: inline-block; + max-width: 100%; /* Ensures item stays within container */ + } + + .info-card { + padding: 16px; + } + \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserRelatedContent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserRelatedContent.razor index bf8830f64..92edaefdd 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserRelatedContent.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Feeds/UserRelatedContent.razor @@ -1,17 +1,41 @@ @using MiniSpace.Web.DTO -@using MiniSpace.Web.Areas.Students -@inject IStudentsService StudentsService +@using MiniSpace.Web.Areas.Events +@inject IEventsService EventsService @using MudBlazor - Recommendations - - @if (recommendations != null) + Event Recommendations + + @if (loadingRecommendations) { - @foreach (var recommendation in recommendations) - { - @recommendation - } + + } + else if (recommendations != null && recommendations.Any()) + { + + @foreach (var recommendation in recommendations) + { + + + + + + @recommendation.Name + @recommendation.StartDate.ToString("MMMM dd, yyyy") + @recommendation.Description + + + + } + + } + else + { + No recommendations available at the moment. } @@ -19,17 +43,25 @@ [Parameter] public Guid UserId { get; set; } - private List recommendations; + private List recommendations; + private bool loadingRecommendations = true; protected override async Task OnInitializedAsync() { - // Fetch recommendations based on the user ID recommendations = await FetchRecommendationsAsync(UserId); + loadingRecommendations = false; } - private Task> FetchRecommendationsAsync(Guid userId) + private async Task> FetchRecommendationsAsync(Guid userId) { - // Dummy implementation, replace with actual recommendation logic - return Task.FromResult(new List { "Event 1", "Event 2", "Event 3" }); + try + { + var result = await EventsService.GetUserEventsFeedAsync(userId, pageNumber: 1, pageSize: 10, sortBy: "PublishDate", direction: "asc"); + return result.Items.ToList(); + } + catch (Exception ex) + { + return new List(); + } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Dialogs/FriendsDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Dialogs/FriendsDialog.razor deleted file mode 100644 index 0e19b17b8..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Dialogs/FriendsDialog.razor +++ /dev/null @@ -1,65 +0,0 @@ -@using MiniSpace.Web.Areas.Identity -@using MiniSpace.Web.Areas.Friends -@using MiniSpace.Web.DTO -@using MiniSpace.Web.Models.Events -@using Radzen -@using System.Globalization -@inject Radzen.DialogService DialogService -@inject IIdentityService IdentityService -@inject IFriendsService FriendsService - - - - - - - - - - - - - - - - - - - - -@code { - [Parameter] - public SearchEventsModel SearchEventsModel { get; set; } - - private IEnumerable friends; - - protected override async Task OnInitializedAsync() - { - await base.OnInitializedAsync(); - friends = (await FriendsService.GetAllFriendsAsync(IdentityService.GetCurrentUserId())).Select(f => f.StudentDetails); - foreach (var friend in friends) - { - if (SearchEventsModel.Friends.Contains(friend.Id)) - { - friend.Selected = true; - } - } - StateHasChanged(); - } - - private void Close() - { - DialogService.Close(); - } - - private void SelectFriends() - { - SearchEventsModel.Friends = friends.Where(f => f.Selected).Select(f => f.Id).ToHashSet(); - DialogService.Close(); - } -} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor index 62cc2f15b..746b2f90f 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor @@ -9,68 +9,69 @@ @inject Radzen.DialogService DialogService @using MudBlazor - - - -@code { - private List _items = new List - { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), - new BreadcrumbItem("Search", href: "/friends/search", icon: Icons.Material.Filled.PersonSearch), - new BreadcrumbItem("Friends", href: "/friends", disabled: true, icon: Icons.Material.Filled.LibraryAddCheck), - }; -} - -
-
- + @code { - private List friends; + private List friends = new List(); private string searchTerm; private bool pageInitialized; + private int currentPage = 1; + private int pageSize = 10; + private int totalFriends; + private bool hasMorePages => friends.Count < totalFriends; + + private List _items = new List + { + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Search", href: "/friends/search", icon: Icons.Material.Filled.PersonSearch), + new BreadcrumbItem("Friends", href: "/friends", disabled: true, icon: Icons.Material.Filled.LibraryAddCheck), + }; protected override async Task OnInitializedAsync() { await IdentityService.InitializeAuthenticationState(); if (IdentityService.IsAuthenticated) - { - try - { - var studentId = IdentityService.GetCurrentUserId(); - var friendsResult = await FriendsService.GetAllFriendsAsync(studentId); - friends = friendsResult?.ToList() ?? new List(); - } - catch (Exception ex) - { - NotificationService.Notify(Radzen.NotificationSeverity.Error, "Failed to Load Friends", $"An error occurred: {ex.Message}", 5000); - friends = new List(); - } + { + await LoadFriends(); pageInitialized = true; } else @@ -224,8 +226,8 @@ private async Task RemoveFriend(Guid friendId) { await FriendsService.RemoveFriendAsync(friendId); - friends = friends.Where(f => f.StudentId != friendId).ToList(); - NotificationService.Notify(Radzen.NotificationSeverity.Warning, "Friend Removed", $"You have removed a friend.", 5000); + friends = friends.Where(f => f.FriendId != friendId).ToList(); + NotificationService.Notify(Radzen.NotificationSeverity.Warning, "Friend Removed", "You have removed a friend.", 5000); StateHasChanged(); } @@ -253,17 +255,33 @@ } private async Task ReloadFriends() + { + currentPage = 1; + friends.Clear(); + await LoadFriends(); + } + + private async Task LoadFriends() { try { var studentId = IdentityService.GetCurrentUserId(); - var friendsResult = await FriendsService.GetAllFriendsAsync(studentId); - friends = friendsResult?.ToList() ?? new List(); + var result = await FriendsService.GetAllFriendsAsync(studentId, currentPage, pageSize); + if (result != null) + { + friends.AddRange(result.Items); + totalFriends = result.TotalItems; + } } catch (Exception ex) { NotificationService.Notify(Radzen.NotificationSeverity.Error, "Failed to Load Friends", $"An error occurred: {ex.Message}", 5000); - friends = new List(); } } -} + + private async Task LoadMoreFriends() + { + currentPage++; + await LoadFriends(); + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsRequests.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsRequests.razor index fb70e4660..76067733f 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsRequests.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsRequests.razor @@ -10,74 +10,72 @@ @inject Radzen.NotificationService NotificationService @inject IJSRuntime JSRuntime - - - - -@code { - private List _items = new List - { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), - new BreadcrumbItem("Search", href: "/friends/search", icon: Icons.Material.Filled.PersonSearch), - new BreadcrumbItem("Friends", href: "/friends", icon: Icons.Material.Filled.LibraryAddCheck), - new BreadcrumbItem("Requests", href: "/friends/requests", disabled: true, icon: Icons.Material.Filled.GroupAdd), - }; -} - -

Incoming Friend Requests

- -
-
- + @code { private string searchTerm; private List students = new List(); - private IEnumerable sentRequests; - private StudentDto student; - RadzenNotification notificationComponent; + private IEnumerable sentRequests = Enumerable.Empty(); + private IEnumerable allFriends = Enumerable.Empty(); + private IEnumerable incomingRequests = Enumerable.Empty(); + private bool pageInitialized = false; private int currentPage = 1; private int pageSize = 10; private int totalStudents; - private IEnumerable allFriends; - private IEnumerable incomingRequests; - private bool pageInitialized; - private DotNetObjectReference _dotNetRef; - protected override async Task OnInitializedAsync() + private List _items = new List + { + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Search", href: "/friends/search", disabled: true, icon: Icons.Material.Filled.PersonSearch) + }; + + protected override async Task OnInitializedAsync() { - await IdentityService.InitializeAuthenticationState(); + await IdentityService.InitializeAuthenticationState(); if (IdentityService.IsAuthenticated) { - sentRequests = await FriendsService.GetSentFriendRequestsAsync(); - incomingRequests = await FriendsService.GetIncomingFriendRequestsAsync(); - allFriends = await FriendsService.GetAllFriendsAsync(IdentityService.GetCurrentUserId()); - - await LoadStudents(); - pageInitialized = true; - StateHasChanged(); - - _dotNetRef = DotNetObjectReference.Create(this); - await JSRuntime.InvokeVoidAsync("infiniteScroll.initialize", _dotNetRef); + try + { + await LoadStudents(); + } + catch (Exception ex) + { + Snackbar.Add($"Failed to Load Data: {ex.Message}", Severity.Error); + } + finally + { + pageInitialized = true; + } } else { @@ -250,49 +241,59 @@ return string.IsNullOrEmpty(profileImageUrl) ? "images/default_profile_image.webp" : profileImageUrl; } - private async Task LoadStudents(string searchArgument = null) + private async Task LoadStudents(string searchArgument = null) { var response = await FriendsService.GetAllStudentsAsync(currentPage, pageSize, searchArgument); - if (response != null) { + + // Log the response object to the console + var jsonResponse = System.Text.Json.JsonSerializer.Serialize(response, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }); + Console.WriteLine("Response JSON:"); + Console.WriteLine(jsonResponse); + + if (response?.Results != null) + { students.AddRange(response.Results); - totalStudents = response.Total; - StateHasChanged(); + totalStudents = response.Total; } } - [JSInvokable] - public async Task LoadMoreData() + private async Task SearchFriends() { - currentPage++; - await LoadStudents(searchTerm); - } - - private async Task SearchFriends() { currentPage = 1; students.Clear(); await LoadStudents(searchTerm); } - private async Task ConnectWithStudent(Guid studentId, MouseEventArgs e) + private async Task ConnectWithStudent(Guid studentId, MouseEventArgs e) { - var currentUserId = IdentityService.GetCurrentUserId(); - await FriendsService.InviteStudent(currentUserId, studentId); + try + { + var currentUserId = IdentityService.GetCurrentUserId(); + await FriendsService.InviteStudent(currentUserId, studentId); + + var student = students.FirstOrDefault(s => s.Id == studentId); + if (student != null) + { + student.InvitationSent = true; + } - var student = students.FirstOrDefault(s => s.Id == studentId); - if (student != null) + sentRequests = (await FriendsService.GetSentFriendRequestsAsync(currentPage, pageSize))?.Items ?? Enumerable.Empty(); + + Snackbar.Add("The invitation has been successfully sent.", Severity.Success); + await JSRuntime.InvokeVoidAsync("playNotificationSound"); + } + catch (Exception ex) + { + Snackbar.Add($"Failed to Send Invitation: {ex.Message}", Severity.Error); + } + finally { - student.InvitationSent = true; - student.IsInvitationPending = true; + StateHasChanged(); } - sentRequests = await FriendsService.GetSentFriendRequestsAsync(); - NotificationService.Notify(Radzen.NotificationSeverity.Success, "Invitation Sent", "The invitation has been successfully sent.", 10000); - await JSRuntime.InvokeVoidAsync("playNotificationSound"); - StateHasChanged(); } private void ViewDetails(Guid studentId) { NavigationManager.NavigateTo($"/user-details/{studentId}"); } - } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/OLD_FRINED_SEARCH_BAD_PAGIANTION.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/OLD_FRINED_SEARCH_BAD_PAGIANTION.razor deleted file mode 100644 index 57db8f031..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/OLD_FRINED_SEARCH_BAD_PAGIANTION.razor +++ /dev/null @@ -1,341 +0,0 @@ -@page "/bad-friends/bad-search" -@using MiniSpace.Web.HttpClients -@using MudBlazor -@inject NavigationManager NavigationManager -@using MiniSpace.Web.Areas.Friends -@inject IFriendsService FriendsService -@using MiniSpace.Web.DTO -@using MiniSpace.Web.Areas.Identity -@inject IIdentityService IdentityService -@inject Radzen.NotificationService NotificationService -@inject IJSRuntime JSRuntime - - - - -@code { - private List _items = new List - { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), - new BreadcrumbItem("Search", href: "/friends/search", disabled: true, icon: Icons.Material.Filled.PersonSearch) - }; -} - -
-
- - @if (!pageInitialized) - { -
- -
- } -
- @foreach (var student in students) - { -
-
- Student Image -
-
@student.FirstName @student.LastName
-

@student.Email

-
-
- - - Details - - @if (student.Id != IdentityService.GetCurrentUserId() && !sentRequests.Any(r => r.InviteeId == student.Id) && !allFriends.Any(f => f.FriendId == student.Id)) - { - - - Connect - - } - else if (allFriends.Any(f => f.FriendId == student.Id)) - { - - - Connected - - } - else if (sentRequests.Any(r => r.InviteeId == student.Id)) - { - - - Pending - - } - else if (incomingRequests.Any(r => r.InviteeId == IdentityService.GetCurrentUserId() && r.State == DTO.States.FriendState.Requested)) - { - - - Incoming Request - - } - else if (student.Id == IdentityService.GetCurrentUserId()) - { - - - It's You - - } -
-
-
- } -
-
-
-
- - - Page @currentPage of @(Math.Ceiling((double)totalStudents / pageSize)) - - -
-
- - - -@code { - private string searchTerm; - private List students = new List(); - private IEnumerable sentRequests; - private StudentDto student; - RadzenNotification notificationComponent; - private int currentPage = 1; - private int pageSize = 10; - private int totalStudents; - private IEnumerable allFriends; - private IEnumerable incomingRequests; - private bool pageInitialized; - - protected override async Task OnInitializedAsync() - { - await IdentityService.InitializeAuthenticationState(); - - if (IdentityService.IsAuthenticated) - { - sentRequests = await FriendsService.GetSentFriendRequestsAsync(); - incomingRequests = await FriendsService.GetIncomingFriendRequestsAsync(); - allFriends = await FriendsService.GetAllFriendsAsync(IdentityService.GetCurrentUserId()); - - await LoadStudents(); - pageInitialized = true; - StateHasChanged(); - } - else - { - NavigationManager.NavigateTo("/login"); - } - } - - private string GetProfileImageUrl(string profileImageUrl) - { - return string.IsNullOrEmpty(profileImageUrl) ? "images/default_profile_image.webp" : profileImageUrl; - } - - private async Task LoadStudents(string searchArgument = null) { - int maxPage = (int)Math.Ceiling((double)totalStudents / pageSize); - if (currentPage > maxPage) currentPage = maxPage; - if (currentPage < 1) currentPage = 1; - - var response = await FriendsService.GetAllStudentsAsync(currentPage, pageSize, searchArgument); - if (response != null) { - students = response.Results; - totalStudents = response.Total; - StateHasChanged(); - } else { - students = new List(); - } - StateHasChanged(); - } - - private void OnDetails(StudentDto selectedStudent) - { - student = selectedStudent; - StateHasChanged(); - } - - private async Task SearchFriends() { - await LoadStudents(searchTerm); - } - - private async Task ConnectWithStudent(Guid studentId, MouseEventArgs e) - { - var currentUserId = IdentityService.GetCurrentUserId(); - await FriendsService.InviteStudent(currentUserId, studentId); - - var student = students.FirstOrDefault(s => s.Id == studentId); - if (student != null) - { - student.InvitationSent = true; - student.IsInvitationPending = true; - } - sentRequests = await FriendsService.GetSentFriendRequestsAsync(); - NotificationService.Notify(Radzen.NotificationSeverity.Success, "Invitation Sent", "The invitation has been successfully sent.", 10000); - await JSRuntime.InvokeVoidAsync("playNotificationSound"); - StateHasChanged(); - } - - private async Task SetPage(int page) - { - if (page < 1 || page > Math.Ceiling((double)totalStudents / pageSize)) { - return; - } - currentPage = page; - student = null; - await LoadStudents(); - } - - private void ViewDetails(Guid studentId) - { - NavigationManager.NavigateTo($"/user-details/{studentId}"); - } - -} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor index 37035ff02..2a09a26fb 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/SentRequests.razor @@ -11,68 +11,66 @@ @inject IJSRuntime JSRuntime - - - + + -@code { - private List _items = new List - { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), - new BreadcrumbItem("Search", href: "/friends/search", icon: Icons.Material.Filled.PersonSearch), - new BreadcrumbItem("Friends", href: "/friends", icon: Icons.Material.Filled.LibraryAddCheck), - new BreadcrumbItem("Requests", href: "/friends/requests", icon: Icons.Material.Filled.GroupAdd), - new BreadcrumbItem("Sent Requests", href: "/friends/sent-requests", disabled: true, icon: Icons.Material.Filled.PersonAddAlt1), - }; -} +

Sent Friend Requests

-
-
- - @if (sentRequests == null) - { -
- -
- } - else if (filteredSentRequests.Any()) - { - @foreach (var request in filteredSentRequests) - { -
-
- Invitee Image -
-
@request.InviteeName
-

@request.InviteeEmail

-

@request.RequestedAt.ToLocalTime().ToString("yyyy-MM-dd")

-

@request.State

-
-
- - - Withdraw - -
-
+
+
+ - } - } - else - { -

No sent requests.

- } -
-
- + @if (!pageInitialized) + { +
+ +
+ } + else if (filteredSentRequests != null && filteredSentRequests.Any()) + { + @foreach (var request in filteredSentRequests) + { +
+
+ Invitee Image +
+
@request.InviteeName
+

@request.InviteeEmail

+

@request.RequestedAt.ToLocalTime().ToString("yyyy-MM-dd")

+

@request.State

+
+
+ + + Withdraw + +
+
+
+ } + + @if (hasMorePages) + { + + + Load More + + } + } + else + { +

No sent requests.

+ } +
+
+ - -@code { - [Parameter] public Guid Id { get; set; } - private StudentDto student; - private bool studentNotFound; - private List _items; - - protected override async Task OnInitializedAsync() - { - _items = new List - { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), - new BreadcrumbItem("Search", href: "/friends/search", icon: Icons.Material.Filled.PersonSearch), - new BreadcrumbItem("Friends", href: "/friends", icon: Icons.Material.Filled.LibraryAddCheck), - new BreadcrumbItem("Requests", href: "/friends/requests", icon: Icons.Material.Filled.GroupAdd), - new BreadcrumbItem("Sent Requests", href: "/friends/sent-requests", icon: Icons.Material.Filled.PersonAddAlt1), - new BreadcrumbItem("Student details", href: $"/user-details/{Id}", disabled: true, icon: Icons.Material.Filled.Person) - }; - - await IdentityService.InitializeAuthenticationState(); - if (IdentityService.IsAuthenticated) - { - student = await FriendsService.GetStudentAsync(Id); - if (student == null) - { - studentNotFound = true; - return; - } - var studentJson = System.Text.Json.JsonSerializer.Serialize(student, new System.Text.Json.JsonSerializerOptions - { - WriteIndented = true // This will format the JSON output with indents for readability - }); - Console.WriteLine("Student Object JSON:"); - Console.WriteLine(studentJson); - } - else - { - NavigationManager.NavigateTo("/login"); - } - } - - private string GetProfileImageUrl(string profileImageUrl) - { - return string.IsNullOrEmpty(profileImageUrl) ? "images/default_profile_image.webp" : profileImageUrl; - } - - private bool IsValidImageUrl(string url) - { - if (string.IsNullOrEmpty(url)) - return false; - - string[] validExtensions = { ".jpg", ".jpeg", ".png", ".gif", ".webp" }; - string extension = System.IO.Path.GetExtension(url)?.ToLower(); - return validExtensions.Contains(extension); - } - - private async Task ReportStudentProfile(StudentDto studentDto) - { - var createReportModel = new CreateReportModel - { - IssuerId = IdentityService.GetCurrentUserId(), - TargetId = studentDto.Id, - TargetOwnerId = studentDto.Id, - ContextType = "StudentProfile" - }; - - await DialogService.OpenAsync("Report profile of the student:", - new Dictionary() { { "CreateReportModel", createReportModel } }, - new DialogOptions() - { - Width = "700px", Height = "350px", Resizable = true, Draggable = true, - AutoFocusFirstElement = false - }); - } - - private bool ShouldDisplayGallery(Visibility galleryVisibility) - { - return galleryVisibility == Visibility.Everyone || (galleryVisibility == Visibility.Connections && IsFriend()); - } - - private bool ShouldDisplayProfileImage(Visibility profileImageVisibility) - { - return profileImageVisibility == Visibility.Everyone || (profileImageVisibility == Visibility.Connections && IsFriend()); - } - - private bool ShouldDisplayBannerImage(Visibility bannerImageVisibility) - { - return bannerImageVisibility == Visibility.Everyone || (bannerImageVisibility == Visibility.Connections && IsFriend()); - } - - private bool IsFriend() - { - // Implement logic to determine if the current user is a friend - // For now, returning true for demonstration purposes - return true; - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/UserDetails.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/UserDetails.razor new file mode 100644 index 000000000..c48d298ba --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/UserDetails.razor @@ -0,0 +1,681 @@ +@page "/user-details/{Id:guid}" +@using MiniSpace.Web.Areas.Friends +@using MiniSpace.Web.Areas.Organizations +@using MiniSpace.Web.Areas.Students +@inject NavigationManager NavigationManager +@inject IFriendsService FriendsService +@inject IEventsService EventsService +@inject IOrganizationsService OrganizationsService +@inject ISnackbar Snackbar +@inject IIdentityService IdentityService +@inject IStudentsService StudentsService +@using MiniSpace.Web.DTO +@using MudBlazor + + + + @if (studentNotFound) + { + + Student profile not found! + It may have been deleted or is inaccessible. + + } + else if (student == null) + { + + } + else + { + + + + + + + @student.FirstName @student.LastName + + @if (!string.IsNullOrWhiteSpace(student.Description)) + { + @student.Description + } + + + + @if (student.DateOfBirth.HasValue) + { + Date of Birth: @student.DateOfBirth?.ToLocalTime().ToString("yyyy-MM-dd") + } + + @if (!string.IsNullOrWhiteSpace(student.City) || !string.IsNullOrWhiteSpace(student.Country)) + { + Location: @student.City, @student.Country + } + + @if (!string.IsNullOrWhiteSpace(student.State)) + { + State: @student.State + } + + + Joined: @student.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd") + + + + + + + @if (ShouldDisplayGallery(student.UserSettings.GalleryVisibility) && student.GalleryOfImageUrls?.Any(img => IsValidImageUrl(img.ImageUrl)) == true) + { + + + Gallery + + + + @foreach (var galleryImage in student.GalleryOfImageUrls.Where(img => IsValidImageUrl(img.ImageUrl))) + { + + + + + + } + + + + } + + + + @if (followers?.Any() == true) + { + + + Followers + + + + @foreach (var follower in followers) + { + + + + + @follower.StudentDetails.FirstName @follower.StudentDetails.LastName + + } + + + + } + + + + @if (following?.Any() == true) + { + + + Following + + + + @foreach (var follow in following) + { + + + + + @follow.StudentDetails.FirstName @follow.StudentDetails.LastName + + } + + + + } + + + + @if (friends?.Any() == true) + { + + + Friends + + + + @foreach (var friend in friends) + { + + + + + @friend.StudentDetails.FirstName @friend.StudentDetails.LastName + + } + + + + } + + + + + + @if (sentFriendRequests?.Any() == true) + { + + + Sent Friend Requests + + + + @foreach (var request in sentFriendRequests) + { + + @request.InviteeName + + } + + + + } + + + @if (incomingFriendRequests?.Any() == true) + { + + + Incoming Friend Requests + + + + @foreach (var request in incomingFriendRequests) + { + + @request.InviterName + + } + + + + } + + + + + + @if (InterestedInEventsDetails?.Any() == true) + { + + + Interested Events + + + + @foreach (var eventDetail in InterestedInEventsDetails) + { + + + + + + + @eventDetail.Name - @eventDetail.StartDate.ToString("MMMM dd, yyyy") + + + + } + + + + } + + + @if (userEvents?.Any() == true) + { + + + Signed Up Events + + + + @foreach (var eventDto in userEvents) + { + + + + + + + @eventDto.Name - @eventDto.StartDate.ToString("MMMM dd, yyyy") + + + + } + + + + } + + + + + + @if (userOrganizations?.Any() == true) + { + + + User's Organizations + + + + @foreach (var organization in userOrganizations) + { + + + + + + + + @organization.Name + @organization.Description + @organization.UserCount users + + + + } + + + + } + + + + @if (followedOrganizations?.Any() == true) + { + + + Organizations Followed + + + + @foreach (var organization in followedOrganizations) + { + + + + + + + + @organization.OrganizationDetails.Name + @organization.OrganizationDetails.Description + @organization.OrganizationDetails.Users.Count() users + + + + } + + + + } + + + } + + + + + +@code { + [Parameter] public Guid Id { get; set; } + private StudentDto student; + private Guid currentUserId; + private bool studentNotFound; + private List userEvents = new List(); + private List InterestedInEventsDetails = new List(); + private List userOrganizations = new List(); + private List followedOrganizations = new List(); + private List friends; + private List followers; + private List following; + private List sentFriendRequests; + private List incomingFriendRequests; + + protected override async Task OnInitializedAsync() + { + await IdentityService.InitializeAuthenticationState(); + if (IdentityService.IsAuthenticated) + { + currentUserId = IdentityService.GetCurrentUserId(); + student = await FriendsService.GetStudentAsync(Id); + if (student == null) + { + studentNotFound = true; + } + else + { + await LoadUserData(); + } + } + else + { + NavigationManager.NavigateTo("/login"); + } + } + + private async Task LoadUserData() + { + var pagedEvents = await EventsService.GetUserEventsAsync(Id, 1, 10, "signed_up"); + userEvents = pagedEvents?.Items?.ToList() ?? new List(); + + friends = (await FriendsService.GetAllFriendsAsync(Id, 1, 10))?.Items?.ToList() ?? new List(); + followers = (await FriendsService.GetPagedFollowersAsync(Id, 1, 10))?.Items?.ToList() ?? new List(); + following = (await FriendsService.GetPagedFollowingAsync(Id, 1, 10))?.Items?.ToList() ?? new List(); + + var sentRequestsPaged = await FriendsService.GetSentFriendRequestsAsync(1, 10); + sentFriendRequests = sentRequestsPaged?.Items?.ToList() ?? new List(); + + var incomingRequestsPaged = await FriendsService.GetIncomingFriendRequestsAsync(1, 10); + incomingFriendRequests = incomingRequestsPaged?.Items?.ToList() ?? new List(); + + await LoadInterestedAndSignedUpEventsAsync(); + await LoadUserOrganizationsAsync(); + await LoadFollowedOrganizationsAsync(); + } + + private async Task LoadInterestedAndSignedUpEventsAsync() + { + try + { + // Load interested events + if (student.InterestedInEvents != null && student.InterestedInEvents.Any()) + { + foreach (var eventId in student.InterestedInEvents) + { + var eventDetail = await EventsService.GetEventAsync(eventId); + if (eventDetail != null) + { + InterestedInEventsDetails.Add(eventDetail); + } + } + } + + // Load signed-up events + if (student.SignedUpEvents != null && student.SignedUpEvents.Any()) + { + foreach (var eventId in student.SignedUpEvents) + { + var eventDetail = await EventsService.GetEventAsync(eventId); + if (eventDetail != null) + { + userEvents.Add(eventDetail); + } + } + } + } + catch (Exception ex) + { + Snackbar.Add($"Error loading events: {ex.Message}", Severity.Error); + } + } + + private async Task LoadUserOrganizationsAsync() + { + try + { + var pagedOrganizations = await OrganizationsService.GetPaginatedUserOrganizationsAsync(Id, 1, 10); + userOrganizations = pagedOrganizations?.Items?.ToList() ?? new List(); + } + catch (Exception ex) + { + Snackbar.Add($"Error loading user's organizations: {ex.Message}", Severity.Error); + } + } + + private async Task LoadFollowedOrganizationsAsync() + { + try + { + var organizations = await OrganizationsService.GetUserFollowedOrganizationsAsync(Id); + followedOrganizations = organizations?.ToList() ?? new List(); + } + catch (Exception ex) + { + Snackbar.Add($"Error loading followed organizations: {ex.Message}", Severity.Error); + } + } + + private string GetProfileImageUrl(string profileImageUrl) + { + return string.IsNullOrEmpty(profileImageUrl) ? "/images/default_profile_image.webp" : profileImageUrl; + } + + private string GetBannerImageUrl(string bannerImageUrl) + { + return string.IsNullOrEmpty(bannerImageUrl) ? "/images/default_banner_image.png" : bannerImageUrl; + } + + private bool IsValidImageUrl(string url) + { + if (string.IsNullOrEmpty(url)) + return false; + + string[] validExtensions = { ".jpg", ".jpeg", ".png", ".gif", ".webp" }; + string extension = System.IO.Path.GetExtension(url)?.ToLower(); + return validExtensions.Contains(extension); + } + + private bool IsFriend(FriendDto friend) + { + return friends.Any(f => f.FriendId == friend.FriendId); + } + + private bool ShouldDisplayGallery(Visibility galleryVisibility) + { + return galleryVisibility == Visibility.Everyone || (galleryVisibility == Visibility.Connections && IsFriend(null)); + } + + private async Task AddFriend() + { + if (student != null) + { + await FriendsService.AddFriendAsync(student.Id); + Snackbar.Add("Friend request sent.", Severity.Success); + } + } + + private async Task BlockUser() + { + if (student != null) + { + await StudentsService.BlockUserAsync(currentUserId, student.Id); + Snackbar.Add("User blocked.", Severity.Warning); + } + } + + private void ReportUser() + { + // Implement the logic to report the user here + Snackbar.Add("User reported.", Severity.Info); + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor index 636f670fd..02c3c036e 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Index.razor @@ -29,12 +29,6 @@ } - @* - Mini Space - @titles[activeIndex] - @descriptions[activeIndex] - Get Started - *@

Welcome to Mini Space

@titles[activeIndex]

@@ -131,4 +125,6 @@ "Connect with friends and family, share your experiences.", "Share your adventures and stories with a global audience." }; + + } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/AllNotifications.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/AllNotifications.razor index 9baa76747..ccdf98437 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/AllNotifications.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/AllNotifications.razor @@ -3,80 +3,114 @@ @inject INotificationsService NotificationsService @inject NavigationManager NavigationManager @using MiniSpace.Web.DTO.Notifications -@using Radzen -@using System.Linq @inject IIdentityService IdentityService @using MudBlazor +@using System.Collections.Generic +@using System.Threading.Tasks - -@code { - private List _items = new List - { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), - new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications) - }; -} + -
-

All Notifications

-
- -@if (notifications == null) -{ -
- - - + @code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications) + }; + } + +
+

All Notifications

-} -else if (notifications.Any()) -{ - - - - - - - - - - - - - - -} -else -{ -

No notifications found.

-} + @if (notifications == null) + { +
+ +
+ } + else if (notifications.Any()) + { + + @foreach (var notification in notifications) + { + + + + + @notification.Message + @notification.CreatedAt.ToString("MMMM dd, yyyy") + + + + + + @if (notification.Status == "Unread") + { + + + Mark as Read + + } + else + { + + + Mark as Unread + + } + + + + + Delete + + + + + } + + } + else + { +

No notifications found.

+ } + + + @code { - private List notifications; + private List notifications = new(); private int currentPage = 1; private int pageSize = 10; private int totalNotifications; @@ -85,8 +119,8 @@ else { await IdentityService.InitializeAuthenticationState(); if (IdentityService.IsAuthenticated) - { - await LoadNotifications(new LoadDataArgs { Skip = 0, Top = pageSize, OrderBy = "createdAt desc" }); + { + await LoadNotifications(); } else { @@ -94,16 +128,8 @@ else } } - - private async Task LoadNotifications(LoadDataArgs args) + private async Task LoadNotifications() { - - var skip = args.Skip ?? 0; - var top = args.Top ?? pageSize; - - currentPage = (skip / top) + 1; - pageSize = top; - var userId = IdentityService.GetCurrentUserId(); var response = await NotificationsService.GetNotificationsByUserAsync(userId, page: currentPage, pageSize: pageSize, sortOrder: "desc", status: null); @@ -111,45 +137,25 @@ else { notifications = response.Results; totalNotifications = response.Total; - StateHasChanged(); + StateHasChanged(); } } - - private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, bool isUnread) + private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, string newStatus) { - string newStatus = isUnread ? "Unread" : "Read"; await NotificationsService.UpdateNotificationStatusAsync(userId, notificationId, newStatus); - - // Refresh the notifications to reflect the change - var notification = notifications.FirstOrDefault(n => n.NotificationId == notificationId); + var notification = notifications.Find(n => n.NotificationId == notificationId); if (notification != null) { notification.Status = newStatus; - await LoadNotifications(new LoadDataArgs { Skip = (currentPage - 1) * pageSize, Top = pageSize, OrderBy = "createdAt desc" }); - StateHasChanged(); // - await OnInitializedAsync(); - } - else - { - StateHasChanged(); // This will force the UI to update if for some reason the notification is not found - await OnInitializedAsync(); + StateHasChanged(); } } - - - - private async Task DeleteNotification(Guid userId, Guid notificationId) + private async Task DeleteNotification(Guid userId, Guid notificationId) { await NotificationsService.DeleteNotificationAsync(userId, notificationId); - // Use FindIndex to locate the specific notification and remove it if found - int index = notifications.FindIndex(n => n.NotificationId == notificationId); - if (index != -1) - { - notifications.RemoveAt(index); - StateHasChanged(); // Update UI to reflect the removal - } + notifications.RemoveAll(n => n.NotificationId == notificationId); + StateHasChanged(); } - } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/HistoryNotifications.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/HistoryNotifications.razor index 33cbb9639..0f61f1977 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/HistoryNotifications.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/HistoryNotifications.razor @@ -3,88 +3,124 @@ @inject INotificationsService NotificationsService @inject NavigationManager NavigationManager @using MiniSpace.Web.DTO.Notifications -@using Radzen -@using System.Linq -@inject IIdentityService IdentityService @using MudBlazor +@inject IIdentityService IdentityService +@using System.Collections.Generic +@using System.Threading.Tasks - -@code { - private List _items = new List - { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), - new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications), - new BreadcrumbItem("New Notifications", href: "/notifications/new", icon: Icons.Material.Filled.NotificationsActive), - new BreadcrumbItem("Notifications History", href: "/notifications/history", disabled: true, icon: Icons.Material.Filled.NotificationsPaused) - }; -} + -
-

Notifications History

-
- -@if (notifications == null) -{ -
- - - + @code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications), + new BreadcrumbItem("New Notifications", href: "/notifications/new", icon: Icons.Material.Filled.NotificationsActive), + new BreadcrumbItem("Notifications History", href: "/notifications/history", disabled: true, icon: Icons.Material.Filled.NotificationsPaused) + }; + } + +
+

Notifications History

-} -else if (notifications.Any()) -{ - - - - - - - - - - - - - - -} -else -{ -

No notifications found.

-} + @if (notifications == null) + { +
+ +
+ } + else if (notifications.Any()) + { + + @foreach (var notification in notifications) + { + + + + + @notification.Message + @notification.CreatedAt.ToString("MMMM dd, yyyy") + + + + + @if (notification.Status == "Unread") + { + + + Mark as Read + + } + else + { + + + Mark as Unread + + } + + + + Delete + + + + + } + + } + else + { +

No notifications found.

+ } + + + @code { - private List notifications; + private List notifications = new(); private int currentPage = 1; private int pageSize = 10; private int totalNotifications; protected override async Task OnInitializedAsync() { + await IdentityService.InitializeAuthenticationState(); if (IdentityService.IsAuthenticated) { - await LoadNotifications(new LoadDataArgs { Skip = 0, Top = pageSize, OrderBy = "createdAt desc" }); + await LoadNotifications(); } else { @@ -92,16 +128,8 @@ else } } - - private async Task LoadNotifications(LoadDataArgs args) + private async Task LoadNotifications() { - - var skip = args.Skip ?? 0; - var top = args.Top ?? pageSize; - - currentPage = (skip / top) + 1; - pageSize = top; - var userId = IdentityService.GetCurrentUserId(); var response = await NotificationsService.GetNotificationsByUserAsync(userId, page: currentPage, pageSize: pageSize, sortOrder: "desc", status: "Read"); @@ -109,16 +137,19 @@ else { notifications = response.Results; totalNotifications = response.Total; - StateHasChanged(); + StateHasChanged(); } } - - private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, string newStatus) + private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, string newStatus) { await NotificationsService.UpdateNotificationStatusAsync(userId, notificationId, newStatus); - notifications.Find(n => n.NotificationId == notificationId).Status = newStatus; - StateHasChanged(); + var notification = notifications.Find(n => n.NotificationId == notificationId); + if (notification != null) + { + notification.Status = newStatus; + StateHasChanged(); + } } private async Task DeleteNotification(Guid userId, Guid notificationId) diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/NewNotifications.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/NewNotifications.razor index adf3e8c14..dd11b8748 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/NewNotifications.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/NewNotifications.razor @@ -3,77 +3,113 @@ @inject INotificationsService NotificationsService @inject NavigationManager NavigationManager @using MiniSpace.Web.DTO.Notifications -@using Radzen -@using System.Linq @inject IIdentityService IdentityService @using MudBlazor +@using System.Collections.Generic +@using System.Threading.Tasks - -@code { - private List _items = new List - { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), - new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications), - new BreadcrumbItem("New Notifications", href: "/notifications/new", disabled: true, icon: Icons.Material.Filled.NotificationsActive) - }; -} + + + @code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications), + new BreadcrumbItem("New Notifications", href: "/notifications/new", disabled: true, icon: Icons.Material.Filled.NotificationsActive) + }; + } -
-

Recent Notifications

-
- -@if (notifications == null) -{ -
- - - +
+

Recent Notifications

-} -else if (notifications.Any()) -{ - - - - - - - - - - - - - - -} -else -{ -

No notifications found.

-} + + @if (notifications == null) + { +
+ +
+ } + else if (notifications.Any()) + { + + @foreach (var notification in notifications) + { + + + + + @notification.Message + @notification.CreatedAt.ToString("MMMM dd, yyyy") + + + + + @if (notification.Status == "Unread") + { + + + Mark as Read + + } + else + { + + + Mark as Unread + + } + + + + Delete + + + + + } + + } + else + { +

No notifications found.

+ } + + + @code { - private List notifications; + private List notifications = new(); private int currentPage = 1; private int pageSize = 10; private int totalNotifications; @@ -82,8 +118,8 @@ else { await IdentityService.InitializeAuthenticationState(); if (IdentityService.IsAuthenticated) - { - await LoadNotifications(new LoadDataArgs { Skip = 0, Top = pageSize, OrderBy = "createdAt desc" }); + { + await LoadNotifications(); } else { @@ -91,15 +127,8 @@ else } } - - private async Task LoadNotifications(LoadDataArgs args) + private async Task LoadNotifications() { - var skip = args.Skip ?? 0; - var top = args.Top ?? pageSize; - - currentPage = (skip / top) + 1; - pageSize = top; - var userId = IdentityService.GetCurrentUserId(); var response = await NotificationsService.GetNotificationsByUserAsync(userId, page: currentPage, pageSize: pageSize, sortOrder: "desc", status: "Unread"); @@ -107,16 +136,19 @@ else { notifications = response.Results; totalNotifications = response.Total; - StateHasChanged(); + StateHasChanged(); } } - - private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, string newStatus) + private async Task UpdateNotificationStatus(Guid userId, Guid notificationId, string newStatus) { await NotificationsService.UpdateNotificationStatusAsync(userId, notificationId, newStatus); - notifications.Find(n => n.NotificationId == notificationId).Status = newStatus; - StateHasChanged(); + var notification = notifications.Find(n => n.NotificationId == notificationId); + if (notification != null) + { + notification.Status = newStatus; + StateHasChanged(); + } } private async Task DeleteNotification(Guid userId, Guid notificationId) diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/Notification.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/Notification.razor index f137b2d25..f90a0511e 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/Notification.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Notifications/Notification.razor @@ -14,7 +14,7 @@ @code { private List _items = new List { - new BreadcrumbItem("Home", href: "/", icon: Icons.Material.Filled.Home), + new BreadcrumbItem("Home", href: "/home", icon: Icons.Material.Filled.Home), new BreadcrumbItem("Notifications", href: "/notifications/all", icon: Icons.Material.Filled.Notifications), new BreadcrumbItem("New Notifications", href: "/notifications/new", icon: Icons.Material.Filled.NotificationsActive), new BreadcrumbItem("Notifications History", href: "/notifications/history", icon: Icons.Material.Filled.NotificationsPaused), diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/CreateOrganization.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/CreateOrganization.razor index db46394ab..1c23af660 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/CreateOrganization.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/CreateOrganization.razor @@ -15,42 +15,42 @@ @using System.Threading.Tasks -Create New Organization - - - - - - -
- - + + @if (_parentOrganizations != null) { - + @foreach (var org in _parentOrganizations) + { + + } } - } - -
+ +
- @if (organizationModel.Settings != null) - { - Private Organization - Visible to Public - } + @if (organizationModel.Settings != null) + { + Private Organization + Visible to Public + } - - Create Organization - + + Create Organization + - - Back - - - + + Back + + + @code { @@ -65,10 +65,7 @@ try { var userId = IdentityService.GetCurrentUserId(); - var userOrganizations = await OrganizationsService.GetUserOrganizationsAsync(userId); - - // Flatten the hierarchy and map to OrganizationDto - _parentOrganizations = FlattenOrganizations(userOrganizations).ToList(); + await LoadUserOrganizationsAsync(userId); selectedParentId = organizationModel.ParentId?.ToString(); if (organizationModel.Settings == null) @@ -82,27 +79,21 @@ } } - private IEnumerable FlattenOrganizations(IEnumerable userOrganizations) + private async Task LoadUserOrganizationsAsync(Guid userId) { - foreach (var userOrg in userOrganizations) + var page = 1; + var pageSize = 100; + var allOrganizations = new List(); + + PagedResult pagedOrganizations; + do { - yield return new OrganizationDto - { - Id = userOrg.Id, - Name = userOrg.Name, - Description = userOrg.Description, - OwnerId = userOrg.OwnerId - // Add any other necessary properties - }; - - if (userOrg.SubOrganizations != null) - { - foreach (var subOrg in FlattenOrganizations(userOrg.SubOrganizations)) - { - yield return subOrg; - } - } - } + pagedOrganizations = await OrganizationsService.GetPaginatedUserOrganizationsAsync(userId, page, pageSize); + allOrganizations.AddRange(pagedOrganizations.Items); + page++; + } while (pagedOrganizations.NextPage.HasValue); + + _parentOrganizations = allOrganizations.ToList(); } private async Task SubmitForm() diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/EventsComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/EventsComponent.razor index 282829e43..33ee6465f 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/EventsComponent.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/EventsComponent.razor @@ -1,3 +1,4 @@ +@page "/organizations/{OrganizationId}/events" @inject IEventsService EventsService @inject NavigationManager NavigationManager @inject IIdentityService IdentityService @@ -101,7 +102,7 @@ } catch (Exception ex) { - Console.Error.WriteLine(ex); + Console.Error.WriteLine(ex); // Log to console for debugging Snackbar.Add($"Failed to load events: {ex.Message}", Severity.Error); } finally @@ -112,18 +113,34 @@ private async Task LoadEvents() { - var searchCommand = new SearchEvents + try { - OrganizationId = OrganizationId, - Pageable = new PageableDto + var searchCommand = new SearchEvents { - Page = 1, - Size = 50 - } - }; + OrganizationId = OrganizationId, + Pageable = new PageableDto + { + Page = 1, + Size = 50 + } + }; + + var result = await EventsService.SearchEventsAsync(searchCommand); + + // Debugging: Log result to ensure data is returned + Console.WriteLine($"Events fetched: {result?.Items.Count()}"); - var result = await EventsService.SearchEventsAsync(searchCommand); - events = result?.Items.ToList() ?? new List(); + events = result?.Items.ToList() ?? new List(); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); // Log to console for debugging + Snackbar.Add($"Failed to load events: {ex.Message}", Severity.Error); + } + finally + { + isLoading = false; + } } private bool CheckIfUserIsAdmin(OrganizationDetailsDto organization) diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/MyOrganizations.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/MyOrganizations.razor index 5bbb796b5..cecd4e798 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/MyOrganizations.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/MyOrganizations.razor @@ -11,54 +11,71 @@ - My Organizations - - - - Create Organization - - - View Tree - - - - @if (_isLoading) - { - - } - else if (_organizations != null && _organizations.Any()) - { - @foreach (var organization in _organizations) + My Organizations + + + + Create Organization + + + View Tree + + + + @if (_isLoading) { - - - - - @organization.Name - @organization.Description - Users: @organization.UserCount - - - - View - - - - + } - } - else - { - No organizations found. - } - - + else if (_organizations != null && _organizations.Any()) + { + @foreach (var organization in _organizations) + { + + + + + + @organization.Name + @organization.Description + Users: @organization.UserCount + + + + View + + + + + } + } + else + { + No organizations found. + } + + + + + + + + Page @_currentPage of @_totalPages + + @code { private bool _isLoading = true; - private List _organizations; + private List _organizations = new List(); + private int _currentPage = 1; + private int _pageSize = 10; + private int _totalPages; + private int _totalItems; + + // Define pageSizeOptions array + private readonly int[] pageSizeOptions = { 6, 10, 15, 20 }; protected override async Task OnInitializedAsync() { @@ -70,10 +87,7 @@ if (IdentityService.IsAuthenticated) { - var userId = IdentityService.GetCurrentUserId(); - var userOrganizations = await OrganizationsService.GetUserOrganizationsAsync(userId); - - _organizations = FlattenOrganizations(userOrganizations).ToList(); + await LoadOrganizationsAsync(); } else { @@ -84,35 +98,53 @@ { Snackbar.Add($"Failed to load organizations: {ex.Message}", Severity.Error); } - - _isLoading = false; + finally + { + _isLoading = false; + } } - private IEnumerable FlattenOrganizations(IEnumerable userOrganizations) + private async Task LoadOrganizationsAsync() { - foreach (var userOrg in userOrganizations) + try { - yield return new OrganizationDto - { - Id = userOrg.Id, - Name = userOrg.Name, - Description = userOrg.Description, - OwnerId = userOrg.OwnerId, - ImageUrl = userOrg.ImageUrl, - BannerUrl = userOrg.BannerUrl, - Users = userOrg.Users - }; - - if (userOrg.SubOrganizations != null) - { - foreach (var subOrg in FlattenOrganizations(userOrg.SubOrganizations)) - { - yield return subOrg; - } - } + var userId = IdentityService.GetCurrentUserId(); + var pagedOrganizations = await OrganizationsService.GetPaginatedUserOrganizationsAsync(userId, _currentPage, _pageSize); + + _organizations = pagedOrganizations?.Items?.ToList() ?? new List(); + _totalPages = pagedOrganizations?.TotalPages ?? 0; + _totalItems = pagedOrganizations?.TotalItems ?? 0; + } + catch (Exception ex) + { + Snackbar.Add($"Error loading organizations: {ex.Message}", Severity.Error); + } + finally + { + _isLoading = false; } } + private async Task OnPageChanged(int page) + { + _currentPage = page; + await LoadOrganizationsAsync(); + } + + private string GetOrganizationImage(string imageUrl) + { + return string.IsNullOrEmpty(imageUrl) + ? "/images/default_organization_profile_image.png" + : imageUrl; + } + + private string GetOrganizationBanner(string bannerUrl) + { + return string.IsNullOrEmpty(bannerUrl) + ? "/images/default_banner_image.png" + : bannerUrl; + } + private void CreateOrganization() { NavigationManager.NavigateTo("/organizations/create"); @@ -122,7 +154,7 @@ { NavigationManager.NavigateTo($"/organizations/details/{organizationId}"); } - + private void ViewTree() { NavigationManager.NavigateTo("/organizations/tree"); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationDetails.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationDetails.razor index 461581195..022148f0c 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationDetails.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationDetails.razor @@ -65,8 +65,9 @@ Posts Events Members - Suborganizations - Gallery + Requests + Suborganizations + Gallery @@ -89,11 +90,15 @@ { } - else if (selectedTabIndex == 5) + else if (selectedTabIndex == 5) { - + } else if (selectedTabIndex == 6) + { + + } + else if (selectedTabIndex == 7) { } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationMembersComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationMembersComponent.razor index 3bb3e7b22..6f6f90a41 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationMembersComponent.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationMembersComponent.razor @@ -30,7 +30,6 @@ @member.FirstName @member.LastName - @* @member.Role?.Name *@ diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationPostsComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationPostsComponent.razor index a2040a8d5..475ee703a 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationPostsComponent.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationPostsComponent.razor @@ -3,6 +3,7 @@ @inject IPostsService PostsService @inject IIdentityService IdentityService @inject IStudentsService StudentsService +@inject IReactionsService ReactionsService @inject NavigationManager NavigationManager @inject ISnackbar Snackbar @using MiniSpace.Web.DTO @@ -11,6 +12,7 @@ @using MiniSpace.Web.Data.Posts @using MiniSpace.Web.DTO.Enums @using MudBlazor +@using System.Linq @if (isLoading) @@ -40,7 +42,7 @@ - + @if (post.UserId.HasValue) { @@ -68,11 +70,54 @@ { } + + @if (reactionsSummaries.TryGetValue(post.Id, out var reactionsSummary)) + { + + @foreach (var reaction in reactionsSummary.ReactionsWithCounts.OrderByDescending(r => r.Value)) + { + + + + @reaction.Value + + + } + + + Total: @reactionsSummary.NumberOfReactions + + + + } - - View - Like - Comment + + + + + + View + + + + + + React + + + + @foreach (var reactionType in Enum.GetValues(typeof(ReactionType)).Cast()) + { + + @reactionType.GetReactionText() + + } + + + + + Comment + @@ -93,6 +138,7 @@ private List posts = new(); private Dictionary studentsCache = new(); + private Dictionary reactionsSummaries = new(); private int currentPage = 1; private int pageSize = 10; private int totalItems = 0; @@ -147,6 +193,8 @@ studentsCache[post.UserId.Value] = student; } } + + reactionsSummaries[post.Id] = await GetReactionsSummaryAsync(post.Id); } } else @@ -170,7 +218,104 @@ } } - private async Task CheckUserPermissionsAsync() + private async Task GetReactionsSummaryAsync(Guid postId) + { + return await ReactionsService.GetReactionsSummaryAsync(postId, ReactionContentType.Post); + } + + private string GetUserAvatar(Guid userId) + { + if (studentsCache.ContainsKey(userId)) + { + return studentsCache[userId].ProfileImageUrl ?? string.Empty; + } + return string.Empty; + } + + private string GetUserName(Guid userId) + { + if (studentsCache.ContainsKey(userId)) + { + return $"{studentsCache[userId].FirstName} {studentsCache[userId].LastName}"; + } + return "Unknown User"; + } + + private async Task OnPageChanged(int newPage) + { + currentPage = newPage; + await LoadOrganizationPostsAsync(); + } + + private void NavigateToPostDetails(Guid postId) + { + NavigationManager.NavigateTo($"/organizations/{OrganizationId}/posts/{postId}"); + } + + private void CreateNewPost() + { + NavigationManager.NavigateTo($"/organizations/{OrganizationId}/posts/create"); + } + + private async Task HandleReactionAsync(PostDto post, ReactionType reactionType) + { + var existingReaction = await GetExistingReactionAsync(post.Id); + + string targetType = post.OrganizationId.HasValue ? "Organization" : "User"; + + if (existingReaction != null) + { + var updateReaction = new UpdateReactionDto + { + ReactionId = existingReaction.Id, + UserId = IdentityService.UserDto.Id, + NewReactionType = reactionType.ToString() + }; + + var updateResult = await ReactionsService.UpdateReactionAsync(updateReaction); + + if (updateResult.IsSuccessStatusCode) + { + Snackbar.Add("Reaction updated successfully!", Severity.Success); + } + else + { + Snackbar.Add($"Failed to update reaction: {updateResult.ErrorMessage?.Reason}", Severity.Error); + } + } + else + { + var createReaction = new CreateReactionDto + { + UserId = IdentityService.UserDto.Id, + ContentId = post.Id, + ContentType = "Post", + ReactionType = reactionType.ToString(), + TargetType = targetType + }; + + var createResult = await ReactionsService.CreateReactionAsync(createReaction); + + if (createResult.IsSuccessStatusCode) + { + Snackbar.Add("Reaction added successfully!", Severity.Success); + } + else + { + Snackbar.Add($"Failed to add reaction: {createResult.ErrorMessage?.Reason}", Severity.Error); + } + } + + await LoadOrganizationPostsAsync(); + } + + private async Task GetExistingReactionAsync(Guid postId) + { + var reactions = await ReactionsService.GetReactionsAsync(postId, ReactionContentType.Post); + return reactions.FirstOrDefault(r => r.UserId == IdentityService.UserDto.Id); + } + + private async Task CheckUserPermissionsAsync() { try { @@ -226,37 +371,4 @@ } } - private string GetUserAvatar(Guid userId) - { - if (studentsCache.ContainsKey(userId)) - { - return studentsCache[userId].ProfileImageUrl ?? string.Empty; - } - return string.Empty; - } - - private string GetUserName(Guid userId) - { - if (studentsCache.ContainsKey(userId)) - { - return $"{studentsCache[userId].FirstName} {studentsCache[userId].LastName}"; - } - return "Unknown User"; - } - - private async Task OnPageChanged(int newPage) - { - currentPage = newPage; - await LoadOrganizationPostsAsync(); - } - - private void NavigateToPostDetails(Guid postId) - { - NavigationManager.NavigateTo($"/organizations/{OrganizationId}/posts/{postId}"); - } - - private void CreateNewPost() - { - NavigationManager.NavigateTo($"/organizations/{OrganizationId}/posts/create"); - } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationRequestsComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationRequestsComponent.razor new file mode 100644 index 000000000..11b768e25 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationRequestsComponent.razor @@ -0,0 +1,164 @@ +@page "/organizations/{OrganizationId:guid}/requests" +@using MudBlazor +@inject ISnackbar Snackbar +@inject IOrganizationsService OrganizationsService +@inject IStudentsService StudentsService + +@code { + [Parameter] + public Guid OrganizationId { get; set; } + + private List requests = new List(); + private Dictionary users = new Dictionary(); + private bool isLoading = true; + private int page = 1; + private int pageSize = 10; + private int totalItems; + + protected override async Task OnInitializedAsync() + { + await LoadRequests(); + } + + private async Task LoadRequests() + { + isLoading = true; + try + { + var result = await OrganizationsService.GetOrganizationRequestsAsync(OrganizationId, page, pageSize); + if (result != null) + { + requests = result.Items.ToList(); + totalItems = result.TotalItems; + + foreach (var request in requests) + { + if (!users.ContainsKey(request.UserId)) + { + var user = await StudentsService.GetStudentAsync(request.UserId); + if (user != null) + { + users[request.UserId] = user; + } + } + } + } + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + Snackbar.Add("Failed to load organization requests.", Severity.Error); + } + finally + { + isLoading = false; + } + } + + private async Task AcceptRequest(Guid requestId) + { + try + { + await OrganizationsService.AcceptFollowRequestAsync(OrganizationId, requestId); + Snackbar.Add("Request accepted successfully.", Severity.Success); + await LoadRequests(); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + Snackbar.Add("Failed to accept request.", Severity.Error); + } + } + + private async Task RejectRequest(Guid requestId) + { + try + { + await OrganizationsService.RejectFollowRequestAsync(OrganizationId, requestId, "Request rejected by admin."); + Snackbar.Add("Request rejected successfully.", Severity.Success); + await LoadRequests(); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + Snackbar.Add("Failed to reject request.", Severity.Error); + } + } + + private void OnPageChanged(int newPage) + { + page = newPage + 1; // Adjust to handle MudTablePager's zero-based index + _ = LoadRequests(); + } + + private string GetUserAvatar(Guid userId) + { + return users.ContainsKey(userId) && !string.IsNullOrEmpty(users[userId]?.ProfileImageUrl) + ? users[userId].ProfileImageUrl + : "/images/default_profile_image.png"; + } +} + + + @if (isLoading) + { + + } + else if (requests == null || !requests.Any()) + { + No requests found for this organization. + } + else + { + + @foreach (var request in requests) + { + + + + + + + + + + + + @users[request.UserId]?.FirstName @users[request.UserId]?.LastName + + + Requested on @request.RequestDate.ToString("g") + + + @request.State + + + Reason: @request.Reason + + + + + + + + Accept + + + Reject + + + + + + + + } + + + + } + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationsFollowing.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationsFollowing.razor index ef0c2cde8..6be2f7eea 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationsFollowing.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationsFollowing.razor @@ -7,40 +7,50 @@ @using System.Threading.Tasks - - Organizations You're Following + + + Organizations You're Following - - @if (_organizations != null && _organizations.Any()) - { - @foreach (var organization in _organizations) + + + + @if (_organizations != null && _organizations.Any()) { - - - - - @organization.OrganizationDetails.Name - @organization.OrganizationDetails.Description - Users: @organization.Users.Count() - - - - View - - - - + @foreach (var organization in _organizations) + { + + + + + + @organization.OrganizationDetails.Name + @organization.OrganizationDetails.Description + Users: @organization.Users.Count() + + + + View + + + + + } } - } - else - { - You're not following any organizations yet. - } - - + else if (_organizations == null) + { + Loading followed organizations... + } + else + { + No followed organizations found. + } + + +
@code { + private string _searchQuery = string.Empty; private IEnumerable _organizations; private bool _isLoading = true; @@ -54,7 +64,7 @@ if (IdentityService.IsAuthenticated) { - _organizations = await OrganizationsService.GetUserFollowedOrganizationsAsync(IdentityService.UserDto.Id); + await SearchOrganizations(); } else { @@ -71,8 +81,48 @@ } } + private async Task SearchOrganizations() + { + try + { + _isLoading = true; + var followedOrganizations = await OrganizationsService.GetUserFollowedOrganizationsAsync(IdentityService.UserDto.Id); + + if (!string.IsNullOrEmpty(_searchQuery)) + { + _organizations = followedOrganizations.Where(o => o.OrganizationDetails.Name.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase)); + } + else + { + _organizations = followedOrganizations; + } + + _isLoading = false; + StateHasChanged(); + } + catch (Exception ex) + { + Console.WriteLine($"Error fetching followed organizations: {ex.Message}"); + _isLoading = false; + } + } + private void NavigateToOrganization(Guid organizationId) { NavigationManager.NavigateTo($"/organizations/details/{organizationId}"); } + + private string GetOrganizationImage(string imageUrl) + { + return string.IsNullOrEmpty(imageUrl) + ? "/images/default_organization_profile_image.png" + : imageUrl; + } + + private string GetOrganizationBanner(string bannerUrl) + { + return string.IsNullOrEmpty(bannerUrl) + ? "/images/default_banner_image.png" + : bannerUrl; + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationsSearch.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationsSearch.razor index 6112b3f43..e4bd99e95 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationsSearch.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/OrganizationsSearch.razor @@ -8,64 +8,65 @@ - - Discover Organizations + + Discover Organizations - + - - @if (_organizations != null && _organizations.Items != null && _organizations.Items.Any()) - { - @foreach (var organization in _organizations.Items) + + @if (_organizations != null && _organizations.Items != null && _organizations.Items.Any()) { - - - - - @organization.Name - @organization.Description - Users: @organization.UserCount - - - - View - - @if (IsUserMemberOfOrganization(organization)) - { - - Member - - } - else - { - - Follow + @foreach (var organization in _organizations.Items) + { + + + + + + @organization.Name + @organization.Description + Users: @organization.UserCount + + + + View - } - - - + @if (IsUserMemberOfOrganization(organization)) + { + + Member + + } + else + { + + Follow + + } + + + + } } - } - else if (_organizations == null || _organizations.Items == null) - { - Loading organizations... - } - else - { - No organizations found. - } - + else if (_organizations == null || _organizations.Items == null) + { + Loading organizations... + } + else + { + No organizations found. + } + - + - + - - Page @_currentPage of @_totalPages - - + + Page @_currentPage of @_totalPages + + @@ -154,4 +155,18 @@ { return organization.Users.Any(user => user.Id == IdentityService.UserDto.Id); } + + private string GetOrganizationImage(string imageUrl) + { + return string.IsNullOrEmpty(imageUrl) + ? "/images/default_organization_profile_image.png" + : imageUrl; + } + + private string GetOrganizationBanner(string bannerUrl) + { + return string.IsNullOrEmpty(bannerUrl) + ? "/images/default_banner_image.png" + : bannerUrl; + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/TreeViewOrganizations.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/TreeViewOrganizations.razor index 94faa95f5..340749298 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/TreeViewOrganizations.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Organizations/TreeViewOrganizations.razor @@ -11,75 +11,75 @@ - - Organization Tree View - - View All Organizations - - - - - @if (_isLoading) - { - - } - else if (_userOrganizations != null && _userOrganizations.Any()) - { - @foreach (var org in _userOrganizations) + + Organization Tree View + + View All Organizations + + + + + @if (_isLoading) { - - - - - - @org.Name - @org.Description - - - - View - - - - - @if (org.SubOrganizations != null && org.SubOrganizations.Any()) - { - - @foreach (var subOrg in org.SubOrganizations) - { - - - - - - @subOrg.Name - @subOrg.Description - - - - View - - - - - - } - - } - + } - } - else - { - No organizations found. - } - + else if (_userOrganizations != null && _userOrganizations.Any()) + { + @foreach (var org in _userOrganizations) + { + + + + + + @org.Name + @org.Description + + + + View + + + + + @if (org.SubOrganizations != null && org.SubOrganizations.Any()) + { + + @foreach (var subOrg in org.SubOrganizations) + { + + + + + + @subOrg.Name + @subOrg.Description + + + + View + + + + + + } + + } + + } + } + else + { + No organizations found. + } + @code { private bool _isLoading = true; - private List _userOrganizations; + private List _userOrganizations; protected override async Task OnInitializedAsync() { @@ -92,7 +92,7 @@ if (IdentityService.IsAuthenticated) { var userId = IdentityService.GetCurrentUserId(); - _userOrganizations = (await OrganizationsService.GetUserOrganizationsAsync(userId)).ToList(); + _userOrganizations = await LoadAllUserOrganizationsAsync(userId); } else { @@ -103,22 +103,72 @@ { Snackbar.Add($"Failed to load organizations: {ex.Message}", Severity.Error); } + finally + { + _isLoading = false; + } + } + + private async Task> LoadAllUserOrganizationsAsync(Guid userId) + { + var page = 1; + var pageSize = 100; + var allOrganizations = new List(); - _isLoading = false; + PagedResult pagedOrganizations; + do + { + pagedOrganizations = await OrganizationsService.GetPaginatedUserOrganizationsAsync(userId, page, pageSize); + allOrganizations.AddRange(pagedOrganizations.Items); + page++; + } while (pagedOrganizations.NextPage.HasValue); + + // Load sub-organizations recursively + foreach (var organization in allOrganizations) + { + await LoadSubOrganizationsAsync(organization); + } + + return allOrganizations; } - private async Task OnExpandChanged(bool expanded, UserOrganizationsDto organization) + private async Task LoadSubOrganizationsAsync(OrganizationDto organization) + { + var subOrganizations = await OrganizationsService.GetAllChildrenOrganizationsAsync(organization.Id); + var subOrganizationDtos = new List(); + + foreach (var subOrgId in subOrganizations) + { + var subOrgDetails = await OrganizationsService.GetOrganizationAsync(subOrgId); + subOrganizationDtos.Add(subOrgDetails); + } + + organization.SubOrganizations = subOrganizationDtos; + + foreach (var subOrg in organization.SubOrganizations) + { + await LoadSubOrganizationsAsync(subOrg); + } + } + + private async Task OnExpandChanged(bool expanded, OrganizationDto organization) { organization.IsExpanded = expanded; - if (expanded && !organization.SubOrganizations.Any() && organization.HasSubOrganizations) + if (expanded && !organization.SubOrganizations.Any()) { - var allChildren = await OrganizationsService.GetAllChildrenOrganizationsAsync(organization.Id); - organization.SubOrganizations = allChildren.Select(id => new UserOrganizationsDto { Id = id }).ToList(); - StateHasChanged(); + await LoadSubOrganizationsAsync(organization); + StateHasChanged(); } } + private string GetOrganizationImage(string imageUrl) + { + return string.IsNullOrEmpty(imageUrl) + ? "/images/default_organization_profile_image.png" + : imageUrl; + } + private void NavigateToOrganization(Guid organizationId) { NavigationManager.NavigateTo($"/organizations/details/{organizationId}"); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostDetails.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostDetails.razor new file mode 100644 index 000000000..5521a365f --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostDetails.razor @@ -0,0 +1,592 @@ +@page "/posts/details/{postId:guid}" +@inject IPostsService PostsService +@inject IStudentsService StudentsService +@inject IIdentityService IdentityService +@inject IReactionsService ReactionsService +@inject ICommentsService CommentsService +@inject NavigationManager NavigationManager +@inject ISnackbar Snackbar +@using Microsoft.JSInterop +@using MiniSpace.Web.DTO +@using MiniSpace.Web.DTO.Wrappers +@using MudBlazor +@using MiniSpace.Web.Areas.Reactions.CommandDto +@using MiniSpace.Web.DTO.Enums +@using System.Linq +@using System.Threading.Tasks +@using System.Collections.Generic + + + + @if (isLoading) + { + + } + else if (post == null) + { + Post not found. + } + else + { + + + + + + + + + @GetUserName(post.UserId) + @post.CreatedAt.ToString("g") + + + + + + + + @if (post.MediaFiles != null && post.MediaFiles.Any()) + { + @foreach (var mediaFile in post.MediaFiles) + { + + } + } + + + + + + + + @foreach (var reaction in reactionsSummary.ReactionsWithCounts.OrderByDescending(r => r.Value)) + { + + + @reaction.Value + + } + + + + + + + + + + + + + Facebook + + + Twitter + + + LinkedIn + + + WhatsApp + + + + + + + @foreach (var reactionType in Enum.GetValues(typeof(ReactionType)).Cast()) + { + + @reactionType.GetReactionText() + + } + + + Apply Reaction + + + + + + + + + + + + + + + + Submit + + + + Total Comments: @totalComments + + +
+ @if (comments.Any()) + { + + @foreach (var comment in comments) + { + + + + + + + @GetUserName(comment.UserId) + @comment.TextContent + @comment.CreatedAt.ToString("g") + + + @($"{comment.Likes?.Count() ?? 0} people liked this comment") + + + + + + + + + + + + + + @if (comment.Id == activeReplyCommentId) + { + + Submit Reply + } + + @if (comments.Any(c => c.ParentId == comment.Id)) + { + + @foreach (var reply in comments.Where(c => c.ParentId == comment.Id)) + { + + + + + + + @GetUserName(reply.UserId) + @reply.TextContent + @reply.CreatedAt.ToString("g") + + + @($"{reply.Likes?.Count() ?? 0} people liked this reply") + + + + + + + + + } + + } + + + + } + + + @if (!isLastPage) + { + + Load More Comments + + } + } + else + { + No comments available. + } +
+
+
+
+ } +
+
+ +@code { + [Parameter] public Guid postId { get; set; } + + private PostDto post; + private ReactionsSummaryDto reactionsSummary; + private List comments = new(); + private Dictionary studentsCache = new(); + private bool isLoading = true; + private Guid? activeReplyCommentId; + private string newCommentText = string.Empty; + private string newReplyText = string.Empty; + private int currentPage = 1; + private int pageSize = 10; + private int totalComments = 0; + private bool isLastPage = false; + private ElementReference commentSection; + private ReactionType selectedReaction = ReactionType.LikeIt; + + protected override async Task OnInitializedAsync() + { + isLoading = true; + + try + { + await IdentityService.InitializeAuthenticationState(); + + if (IdentityService.IsAuthenticated) + { + await LoadPostDetailsAsync(); + } + else + { + NavigationManager.NavigateTo("/signin", forceLoad: true); + } + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + Snackbar.Add($"Failed to load post details: {ex.Message}", Severity.Error); + } + finally + { + isLoading = false; + } + } + + private async Task LoadPostDetailsAsync() + { + try + { + post = await PostsService.GetPostAsync(postId); + + if (post == null) + { + Snackbar.Add("Post not found.", Severity.Warning); + return; + } + + if (post.UserId.HasValue) + { + var student = await StudentsService.GetStudentAsync(post.UserId.Value); + if (student != null) + { + studentsCache[post.UserId.Value] = student; + } + } + + reactionsSummary = await ReactionsService.GetReactionsSummaryAsync(postId, ReactionContentType.Post); + await LoadCommentsAsync(); + } + catch (Exception ex) + { + Snackbar.Add($"Failed to load post details: {ex.Message}", Severity.Error); + } + } + + private void CacheStudentInfo(IEnumerable commentDtos) + { + foreach (var comment in commentDtos) + { + if (!studentsCache.ContainsKey(comment.UserId)) + { + var student = StudentsService.GetStudentAsync(comment.UserId).Result; + if (student != null) + { + studentsCache[comment.UserId] = student; + } + } + + if (comment.Replies != null) + { + foreach (var reply in comment.Replies) + { + if (!studentsCache.ContainsKey(reply.UserId)) + { + var replyAuthor = StudentsService.GetStudentAsync(reply.UserId).Result; + if (replyAuthor != null) + { + studentsCache[reply.UserId] = replyAuthor; + } + } + } + } + } + } + + private async Task LoadMoreCommentsAsync() + { + if (!isLastPage) + { + currentPage++; + await LoadCommentsAsync(); + } + } + + private async Task LoadCommentsAsync() + { + var command = new SearchRootCommentsCommand( + contextId: postId, + commentContext: DetermineCommentContext().ToString(), + pageable: new PageableDto + { + Page = currentPage, + Size = pageSize, + Sort = new SortDto + { + SortBy = new[] { "CreatedAt" }, + Direction = "asc" + } + } + ); + + var response = await CommentsService.SearchRootCommentsAsync(command); + + if (response != null) + { + // Update total comments and check if it is the last page + totalComments = response.TotalItems; + isLastPage = currentPage * pageSize >= totalComments; + + // Append new comments to the existing list + comments.AddRange(response.Items); + CacheStudentInfo(response.Items); + } + else + { + Snackbar.Add("Failed to load comments.", Severity.Error); + } + } + + private string GetUserAvatar(Guid? userId) + { + if (userId.HasValue && studentsCache.ContainsKey(userId.Value)) + { + return studentsCache[userId.Value].ProfileImageUrl ?? string.Empty; + } + return string.Empty; + } + + private string GetUserName(Guid? userId) + { + if (userId.HasValue && studentsCache.ContainsKey(userId.Value)) + { + return $"{studentsCache[userId.Value].FirstName} {studentsCache[userId.Value].LastName}"; + } + return "Unknown User"; + } + + private async Task HandleReactionAsync(ReactionType reactionType) + { + var existingReaction = await GetExistingReactionAsync(post.Id); + + string targetType = post.OrganizationId.HasValue ? "Organization" : "User"; + + if (existingReaction != null) + { + var updateReaction = new UpdateReactionDto + { + ReactionId = existingReaction.Id, + UserId = IdentityService.UserDto.Id, + NewReactionType = reactionType.ToString(), + ContentType = "Post", + TargetType = targetType + }; + + var updateResult = await ReactionsService.UpdateReactionAsync(updateReaction); + + if (updateResult.IsSuccessStatusCode) + { + Snackbar.Add("Reaction updated successfully!", Severity.Success); + reactionsSummary = await ReactionsService.GetReactionsSummaryAsync(postId, ReactionContentType.Post); + } + else + { + Snackbar.Add($"Failed to update reaction: {updateResult.ErrorMessage?.Reason}", Severity.Error); + } + } + else + { + var createReaction = new CreateReactionDto + { + UserId = IdentityService.UserDto.Id, + ContentId = post.Id, + ContentType = "Post", + ReactionType = reactionType.ToString(), + TargetType = targetType + }; + + var createResult = await ReactionsService.CreateReactionAsync(createReaction); + + if (createResult.IsSuccessStatusCode) + { + Snackbar.Add("Reaction added successfully!", Severity.Success); + reactionsSummary = await ReactionsService.GetReactionsSummaryAsync(postId, ReactionContentType.Post); + } + else + { + Snackbar.Add($"Failed to add reaction: {createResult.ErrorMessage?.Reason}", Severity.Error); + } + } + } + + private async Task GetExistingReactionAsync(Guid postId) + { + var reactions = await ReactionsService.GetReactionsAsync(postId, ReactionContentType.Post); + return reactions.FirstOrDefault(r => r.UserId == IdentityService.UserDto.Id); + } + + private void ToggleReplySection(Guid commentId) + { + if (activeReplyCommentId == commentId) + { + activeReplyCommentId = null; + newReplyText = string.Empty; + } + else + { + activeReplyCommentId = commentId; + newReplyText = string.Empty; + } + } + + private CommentContext DetermineCommentContext() + { + if (post.OrganizationId.HasValue) + { + return post.EventId.HasValue ? CommentContext.OrganizationEvent : CommentContext.OrganizationPost; + } + else + { + return post.EventId.HasValue ? CommentContext.UserEvent : CommentContext.UserPost; + } + } + + private async Task SubmitCommentAsync() + { + if (string.IsNullOrWhiteSpace(newCommentText)) + { + Snackbar.Add("Comment cannot be empty.", Severity.Warning); + return; + } + + var command = new CreateCommentCommand( + commentId: Guid.NewGuid(), + contextId: post.Id, + commentContext: DetermineCommentContext().ToString(), + userId: IdentityService.GetCurrentUserId(), + parentId: Guid.Empty, + textContent: newCommentText + ); + + var response = await CommentsService.CreateCommentAsync(command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Comment added successfully!", Severity.Success); + newCommentText = string.Empty; + currentPage = 1; // Reset to the first page + comments.Clear(); // Clear the comments list + await LoadCommentsAsync(); + } + else + { + Snackbar.Add($"Failed to add comment: {response.ErrorMessage?.Reason}", Severity.Error); + } + } + + private async Task SubmitReplyAsync(CommentDto parentComment) + { + if (string.IsNullOrWhiteSpace(newReplyText)) + { + Snackbar.Add("Reply cannot be empty.", Severity.Warning); + return; + } + + var command = new CreateCommentCommand( + commentId: Guid.NewGuid(), + contextId: post.Id, + commentContext: DetermineCommentContext().ToString(), + userId: IdentityService.GetCurrentUserId(), + parentId: parentComment.Id, + textContent: newReplyText + ); + + var response = await CommentsService.CreateCommentAsync(command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Reply added successfully!", Severity.Success); + newReplyText = string.Empty; + currentPage = 1; // Reset to the first page + comments.Clear(); // Clear the comments list + await LoadCommentsAsync(); + } + else + { + Snackbar.Add($"Failed to add reply: {response.ErrorMessage?.Reason}", Severity.Error); + } + } + + private async Task AddLikeToCommentAsync(CommentDto comment) + { + var command = new AddLikeDto(comment.Id, IdentityService.GetCurrentUserId(), DetermineCommentContext().ToString()); + + var response = await CommentsService.AddLikeAsync(command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Comment liked successfully!", Severity.Success); + } + else + { + Snackbar.Add($"Failed to like comment: {response.ErrorMessage?.Reason}", Severity.Error); + } + } + + private async Task AddLikeToReplyAsync(ReplyDto reply) + { + var command = new AddLikeDto(reply.Id, IdentityService.GetCurrentUserId(), DetermineCommentContext().ToString()); + + var response = await CommentsService.AddLikeAsync(command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Reply liked successfully!", Severity.Success); + } + else + { + Snackbar.Add($"Failed to like reply: {response.ErrorMessage?.Reason}", Severity.Error); + } + } + + private void SharePost(string platform) + { + var postUrl = NavigationManager.Uri; + var encodedUrl = Uri.EscapeDataString(postUrl); + var shareUrl = platform switch + { + "facebook" => $"https://www.facebook.com/sharer/sharer.php?u={encodedUrl}", + "twitter" => $"https://twitter.com/intent/tweet?url={encodedUrl}", + "linkedin" => $"https://www.linkedin.com/shareArticle?mini=true&url={encodedUrl}", + "whatsapp" => $"https://api.whatsapp.com/send?text={encodedUrl}", + _ => "" + }; + + if (!string.IsNullOrEmpty(shareUrl)) + { + NavigationManager.NavigateTo(shareUrl, true); + } + else + { + Snackbar.Add("Failed to share the post.", Severity.Error); + } + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostsList.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostsList.razor index 0b0623347..4f83458ae 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostsList.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostsList.razor @@ -2,12 +2,20 @@ @inject IPostsService PostsService @inject IStudentsService StudentsService @inject IIdentityService IdentityService +@inject IReactionsService ReactionsService +@inject ICommentsService CommentsService @inject NavigationManager NavigationManager @inject ISnackbar Snackbar @using MiniSpace.Web.DTO @using MiniSpace.Web.DTO.Wrappers @using MiniSpace.Web.Data.Posts @using MudBlazor +@using MiniSpace.Web.Areas.Reactions.CommandDto +@using MiniSpace.Web.Areas.Comments.CommandsDto +@using MiniSpace.Web.Areas.Comments.CommandDto +@using System.Linq +@using System.Threading.Tasks +@using System.Collections.Generic @@ -31,11 +39,11 @@ - - + + - + @GetUserName(post.UserId) @post.CreatedAt.ToString("g") @@ -47,12 +55,143 @@ { } + + @if (reactionsSummaries.TryGetValue(post.Id, out var reactionsSummary)) + { + + @foreach (var reaction in reactionsSummary.ReactionsWithCounts.OrderByDescending(r => r.Value)) + { + + + + @reaction.Value + + + } + + + Total: @reactionsSummary.NumberOfReactions + + + + } - - View - Like - Comment + + + + + + View + + + + + + React + + + + @foreach (var reactionType in Enum.GetValues(typeof(ReactionType)).Cast()) + { + + @reactionType.GetReactionText() + + } + + + + + Comment + + + @if (post.Id == activeCommentPostId) + { + + + Submit + + @if (postComments.TryGetValue(post.Id, out var comments) && comments.Any()) + { + + @foreach (var comment in comments.Where(c => c.ParentId == Guid.Empty)) + { + @* Render the main comment *@ + + + + + + + @GetUserName(comment.UserId) + @comment.TextContent + @comment.CreatedAt.ToString("g") + + + + @($"{comment.Likes?.Count() ?? 0} people liked this comment") + + + + + Reply + + + + + Like + + + @if (comment.Id == activeReplyCommentId) + { + + Submit Reply + } + + + @if (comments.Any(c => c.ParentId == comment.Id)) + { + + @foreach (var reply in comments.Where(c => c.ParentId == comment.Id)) + { + + + + + + + @GetUserName(reply.UserId) + @reply.TextContent + @reply.CreatedAt.ToString("g") + + + + @($"{reply.Likes?.Count() ?? 0} people liked this reply") + + + + + Like + + + + + } + + + } + + + + } + + } + else + { + No comments available. + } + + } } @@ -69,10 +208,16 @@ @code { private List posts = new(); private Dictionary studentsCache = new(); + private Dictionary reactionsSummaries = new(); + private Dictionary> postComments = new(); private int currentPage = 1; private int pageSize = 10; private int totalItems = 0; private bool isLoading = true; + private Guid? activeCommentPostId; + private Guid? activeReplyCommentId; + private string newCommentText = string.Empty; + private string newReplyText = string.Empty; protected override async Task OnInitializedAsync() { @@ -129,7 +274,7 @@ posts = result.Items.ToList(); totalItems = result.TotalItems; - foreach (var post in posts.Where(p => p.UserId.HasValue)) + foreach (var post in posts) { if (post.UserId.HasValue && !studentsCache.ContainsKey(post.UserId.Value)) { @@ -139,6 +284,12 @@ studentsCache[post.UserId.Value] = student; } } + + // Get reactions summary for each post and store it in the dictionary + reactionsSummaries[post.Id] = await GetReactionsSummaryAsync(post.Id); + + // Load comments for each post + postComments[post.Id] = await LoadCommentsForPostAsync(post); } } else @@ -181,4 +332,262 @@ { NavigationManager.NavigateTo($"/posts/details/{postId}"); } + + private async Task GetReactionsSummaryAsync(Guid postId) + { + return await ReactionsService.GetReactionsSummaryAsync(postId, ReactionContentType.Post); + } + + private async Task HandleReactionAsync(PostDto post, ReactionType reactionType) + { + var existingReaction = await GetExistingReactionAsync(post.Id); + + string targetType = post.OrganizationId.HasValue ? "Organization" : "User"; + + if (existingReaction != null) + { + var updateReaction = new UpdateReactionDto + { + ReactionId = existingReaction.Id, + UserId = IdentityService.UserDto.Id, + NewReactionType = reactionType.ToString(), + ContentType = "Post", + TargetType = targetType + }; + + var updateResult = await ReactionsService.UpdateReactionAsync(updateReaction); + + if (updateResult.IsSuccessStatusCode) + { + Snackbar.Add("Reaction updated successfully!", Severity.Success); + } + else + { + Snackbar.Add($"Failed to update reaction: {updateResult.ErrorMessage?.Reason}", Severity.Error); + } + } + else + { + var createReaction = new CreateReactionDto + { + UserId = IdentityService.UserDto.Id, + ContentId = post.Id, + ContentType = "Post", + ReactionType = reactionType.ToString(), + TargetType = targetType + }; + + var createResult = await ReactionsService.CreateReactionAsync(createReaction); + + if (createResult.IsSuccessStatusCode) + { + Snackbar.Add("Reaction added successfully!", Severity.Success); + } + else + { + Snackbar.Add($"Failed to add reaction: {createResult.ErrorMessage?.Reason}", Severity.Error); + } + } + + await LoadPostsAsync(); + } + + private async Task GetExistingReactionAsync(Guid postId) + { + var reactions = await ReactionsService.GetReactionsAsync(postId, ReactionContentType.Post); + return reactions.FirstOrDefault(r => r.UserId == IdentityService.UserDto.Id); + } + + private void ToggleCommentSection(Guid postId) + { + if (activeCommentPostId == postId) + { + activeCommentPostId = null; + newCommentText = string.Empty; + } + else + { + activeCommentPostId = postId; + newCommentText = string.Empty; + } + } + + private void ToggleReplySection(Guid commentId) + { + if (activeReplyCommentId == commentId) + { + activeReplyCommentId = null; + newReplyText = string.Empty; + } + else + { + activeReplyCommentId = commentId; + newReplyText = string.Empty; + } + } + + private async Task> LoadCommentsForPostAsync(PostDto post) + { + var command = new SearchRootCommentsCommand( + contextId: post.Id, + commentContext: DetermineCommentContext(post).ToString(), + pageable: new PageableDto + { + Page = 1, + Size = 10, + Sort = new SortDto + { + SortBy = new[] { "CreatedAt" }, + Direction = "asc" + } + } + ); + + var response = await CommentsService.SearchRootCommentsAsync(command); + var comments = response.Items?.ToList() ?? new List(); + + // Fetch user details for each comment author and reply author + foreach (var comment in comments) + { + if (!studentsCache.ContainsKey(comment.UserId)) + { + var student = await StudentsService.GetStudentAsync(comment.UserId); + if (student != null) + { + studentsCache[comment.UserId] = student; + } + } + + if (comment.Replies != null) + { + foreach (var reply in comment.Replies) + { + if (!studentsCache.ContainsKey(reply.UserId)) + { + var replyAuthor = await StudentsService.GetStudentAsync(reply.UserId); + if (replyAuthor != null) + { + studentsCache[reply.UserId] = replyAuthor; + } + } + } + } + } + + return comments; + } + + private CommentContext DetermineCommentContext(PostDto post) + { + if (post.OrganizationId.HasValue) + { + return post.EventId.HasValue ? CommentContext.OrganizationEvent : CommentContext.OrganizationPost; + } + else + { + return post.EventId.HasValue ? CommentContext.UserEvent : CommentContext.UserPost; + } + } + + private async Task SubmitCommentAsync(PostDto post) + { + if (string.IsNullOrWhiteSpace(newCommentText)) + { + Snackbar.Add("Comment cannot be empty.", Severity.Warning); + return; + } + + var commentContext = DetermineCommentContext(post); + var userId = IdentityService.GetCurrentUserId(); + + var command = new CreateCommentCommand( + commentId: Guid.NewGuid(), + contextId: post.Id, + commentContext: commentContext.ToString(), + userId: userId, + parentId: Guid.Empty, + textContent: newCommentText + ); + + var response = await CommentsService.CreateCommentAsync(command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Comment added successfully!", Severity.Success); + newCommentText = string.Empty; + await LoadPostsAsync(); + } + else + { + Snackbar.Add($"Failed to add comment: {response.ErrorMessage?.Reason}", Severity.Error); + } + } + + private async Task SubmitReplyAsync(PostDto post, CommentDto parentComment) + { + if (string.IsNullOrWhiteSpace(newReplyText)) + { + Snackbar.Add("Reply cannot be empty.", Severity.Warning); + return; + } + + var commentContext = DetermineCommentContext(post); + var userId = IdentityService.GetCurrentUserId(); + + var command = new CreateCommentCommand( + commentId: Guid.NewGuid(), + contextId: post.Id, + commentContext: commentContext.ToString(), + userId: userId, + parentId: parentComment.Id, + textContent: newReplyText + ); + + var response = await CommentsService.CreateCommentAsync(command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Reply added successfully!", Severity.Success); + newReplyText = string.Empty; + await LoadPostsAsync(); + } + else + { + Snackbar.Add($"Failed to add reply: {response.ErrorMessage?.Reason}", Severity.Error); + } + } + + private async Task AddLikeToCommentAsync(CommentDto comment, PostDto post) + { + var command = new AddLikeDto(comment.Id, IdentityService.UserDto.Id, DetermineCommentContext(post).ToString()); + + var response = await CommentsService.AddLikeAsync(command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Comment liked successfully!", Severity.Success); + await LoadPostsAsync(); + } + else + { + Snackbar.Add($"Failed to like comment: {response.ErrorMessage?.Reason}", Severity.Error); + } + } + + private async Task AddLikeToReplyAsync(ReplyDto reply, PostDto post) + { + var command = new AddLikeDto(reply.Id, IdentityService.UserDto.Id, DetermineCommentContext(post).ToString()); + + var response = await CommentsService.AddLikeAsync(command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Reply liked successfully!", Severity.Success); + await LoadPostsAsync(); + } + else + { + Snackbar.Add($"Failed to like reply: {response.ErrorMessage?.Reason}", Severity.Error); + } + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostsMy.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostsMy.razor index d598e0192..1319626eb 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostsMy.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostsMy.razor @@ -1,81 +1,230 @@ @page "/posts/my" @inject IPostsService PostsService @inject IStudentsService StudentsService -@inject NavigationManager NavigationManager -@inject ISnackbar Snackbar @inject IIdentityService IdentityService +@inject IReactionsService ReactionsService +@inject ICommentsService CommentsService +@inject ISnackbar Snackbar +@inject NavigationManager NavigationManager @using MiniSpace.Web.DTO @using MiniSpace.Web.DTO.Wrappers -@using MiniSpace.Web.Data.Posts @using MudBlazor -@using System.Security.Claims - - - - - - My Posts - +@using System.Linq +@using System.Threading.Tasks +@using System.Collections.Generic +@using MiniSpace.Web.Areas.Reactions +@using MiniSpace.Web.Areas.Reactions.CommandDto +@using MiniSpace.Web.Areas.Comments.CommandsDto +@using MiniSpace.Web.Areas.Comments.CommandDto - @if (posts != null && posts.Any()) + + + @if (isLoading) { - @foreach (var post in posts) - { + + } + else if (posts == null || !posts.Any()) + { + You have no posts yet. + } + else + { + - - - - - - - - @GetUserName(post.UserId.Value) - @post.CreatedAt.ToString("g") - - - - - - @if (post.MediaFiles != null && post.MediaFiles.Any()) + My Posts + + + @foreach (var post in posts) + { + + + + + + + + + @GetUserName(post.UserId.Value) + @post.CreatedAt.ToString("g") + + + + + + @if (post.MediaFiles != null && post.MediaFiles.Any()) + { + + } + + @if (reactionsSummaries.TryGetValue(post.Id, out var reactionsSummary)) + { + + @foreach (var reaction in reactionsSummary.ReactionsWithCounts.OrderByDescending(r => r.Value)) + { + + + + @reaction.Value + + + } + + + Total: @reactionsSummary.NumberOfReactions + + + + } + + + + + + + View + + + + + + React + + + + @foreach (var reactionType in Enum.GetValues(typeof(ReactionType)).Cast()) + { + + @reactionType.GetReactionText() + + } + + + + + Comment + + + + @if (post.Id == activeCommentPostId) { - + + + Submit + + @if (postComments.TryGetValue(post.Id, out var comments) && comments.Any()) + { + + @foreach (var comment in comments.Where(c => c.ParentId == Guid.Empty)) + { + + + + + + + @GetUserName(comment.UserId) + @comment.TextContent + @comment.CreatedAt.ToString("g") + + + + @($"{comment.Likes?.Count() ?? 0} people liked this comment") + + + + + Reply + + + + + Like + + + @if (comment.Id == activeReplyCommentId) + { + + Submit Reply + } + + + @if (comments.Any(c => c.ParentId == comment.Id)) + { + + @foreach (var reply in comments.Where(c => c.ParentId == comment.Id)) + { + + + + + + + @GetUserName(reply.UserId) + @reply.TextContent + @reply.CreatedAt.ToString("g") + + + + @($"{reply.Likes?.Count() ?? 0} people liked this reply") + + + + + Like + + + + + } + + } + + + + } + + } + else + { + No comments available. + } + } - - - View - Like - Comment - - + + + } + + + + + - } + } - else - { - - You have no posts yet. - - } - - - - - - - + + @code { private List posts = new(); private Dictionary studentsCache = new(); + private Dictionary reactionsSummaries = new(); + private Dictionary> postComments = new(); private int currentPage = 1; private int pageSize = 10; private int totalItems = 0; + private bool isLoading = true; + private Guid? activeCommentPostId; + private Guid? activeReplyCommentId; + private string newCommentText = string.Empty; + private string newReplyText = string.Empty; private Guid? currentUserId; protected override async Task OnInitializedAsync() { try { + isLoading = true; + if (IdentityService.IsAuthenticated) { currentUserId = IdentityService.GetCurrentUserId(); @@ -99,6 +248,10 @@ { Snackbar.Add($"Error initializing page: {ex.Message}", Severity.Error); } + finally + { + isLoading = false; + } } private async Task LoadPostsAsync() @@ -134,7 +287,6 @@ posts = result.Items.ToList(); totalItems = result.TotalItems; - // Load student data for posts with UserId foreach (var post in posts.Where(p => p.UserId.HasValue)) { if (!studentsCache.ContainsKey(post.UserId.Value)) @@ -145,6 +297,11 @@ studentsCache[post.UserId.Value] = student; } } + + reactionsSummaries[post.Id] = await ReactionsService.GetReactionsSummaryAsync(post.Id, ReactionContentType.Post); + + // Load comments for each post + postComments[post.Id] = await LoadCommentsForPostAsync(post); } } else @@ -192,4 +349,255 @@ { NavigationManager.NavigateTo($"/posts/details/{postId}"); } + + private async Task HandleReactionAsync(PostDto post, ReactionType reactionType) + { + var existingReaction = await GetExistingReactionAsync(post.Id); + + if (existingReaction != null) + { + var updateReaction = new UpdateReactionDto + { + ReactionId = existingReaction.Id, + UserId = currentUserId.Value, + NewReactionType = reactionType.ToString(), + ContentType = "Post", + TargetType = post.OrganizationId.HasValue ? "Organization" : "User" + }; + + var updateResult = await ReactionsService.UpdateReactionAsync(updateReaction); + + if (updateResult.IsSuccessStatusCode) + { + Snackbar.Add("Reaction updated successfully!", Severity.Success); + } + else + { + Snackbar.Add($"Failed to update reaction: {updateResult.ErrorMessage?.Reason}", Severity.Error); + } + } + else + { + var createReaction = new CreateReactionDto + { + UserId = currentUserId.Value, + ContentId = post.Id, + ContentType = "Post", + ReactionType = reactionType.ToString(), + TargetType = post.OrganizationId.HasValue ? "Organization" : "User" + }; + + var createResult = await ReactionsService.CreateReactionAsync(createReaction); + + if (createResult.IsSuccessStatusCode) + { + Snackbar.Add("Reaction added successfully!", Severity.Success); + } + else + { + Snackbar.Add($"Failed to add reaction: {createResult.ErrorMessage?.Reason}", Severity.Error); + } + } + + await LoadPostsAsync(); + } + + private async Task GetExistingReactionAsync(Guid postId) + { + var reactions = await ReactionsService.GetReactionsAsync(postId, ReactionContentType.Post); + return reactions.FirstOrDefault(r => r.UserId == currentUserId.Value); + } + + private void ToggleCommentSection(Guid postId) + { + if (activeCommentPostId == postId) + { + activeCommentPostId = null; + newCommentText = string.Empty; + } + else + { + activeCommentPostId = postId; + newCommentText = string.Empty; + } + } + + private void ToggleReplySection(Guid commentId) + { + if (activeReplyCommentId == commentId) + { + activeReplyCommentId = null; + newReplyText = string.Empty; + } + else + { + activeReplyCommentId = commentId; + newReplyText = string.Empty; + } + } + + private async Task> LoadCommentsForPostAsync(PostDto post) + { + var command = new SearchRootCommentsCommand( + contextId: post.Id, + commentContext: DetermineCommentContext(post).ToString(), + pageable: new PageableDto + { + Page = 1, + Size = 10, + Sort = new SortDto + { + SortBy = new[] { "CreatedAt" }, + Direction = "asc" + } + } + ); + + var response = await CommentsService.SearchRootCommentsAsync(command); + var comments = response.Items?.ToList() ?? new List(); + + // Fetch user details for each comment author and reply author + foreach (var comment in comments) + { + if (!studentsCache.ContainsKey(comment.UserId)) + { + var student = await StudentsService.GetStudentAsync(comment.UserId); + if (student != null) + { + studentsCache[comment.UserId] = student; + } + } + + if (comment.Replies != null) + { + foreach (var reply in comment.Replies) + { + if (!studentsCache.ContainsKey(reply.UserId)) + { + var replyAuthor = await StudentsService.GetStudentAsync(reply.UserId); + if (replyAuthor != null) + { + studentsCache[reply.UserId] = replyAuthor; + } + } + } + } + } + + return comments; + } + + private CommentContext DetermineCommentContext(PostDto post) + { + if (post.OrganizationId.HasValue) + { + return post.EventId.HasValue ? CommentContext.OrganizationEvent : CommentContext.OrganizationPost; + } + else + { + return post.EventId.HasValue ? CommentContext.UserEvent : CommentContext.UserPost; + } + } + + private async Task SubmitCommentAsync(PostDto post) + { + if (string.IsNullOrWhiteSpace(newCommentText)) + { + Snackbar.Add("Comment cannot be empty.", Severity.Warning); + return; + } + + var commentContext = DetermineCommentContext(post); + var userId = currentUserId.Value; + + var command = new CreateCommentCommand( + commentId: Guid.NewGuid(), + contextId: post.Id, + commentContext: commentContext.ToString(), + userId: userId, + parentId: Guid.Empty, + textContent: newCommentText + ); + + var response = await CommentsService.CreateCommentAsync(command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Comment added successfully!", Severity.Success); + newCommentText = string.Empty; + await LoadPostsAsync(); + } + else + { + Snackbar.Add($"Failed to add comment: {response.ErrorMessage?.Reason}", Severity.Error); + } + } + + private async Task SubmitReplyAsync(PostDto post, CommentDto parentComment) + { + if (string.IsNullOrWhiteSpace(newReplyText)) + { + Snackbar.Add("Reply cannot be empty.", Severity.Warning); + return; + } + + var commentContext = DetermineCommentContext(post); + var userId = currentUserId.Value; + + var command = new CreateCommentCommand( + commentId: Guid.NewGuid(), + contextId: post.Id, + commentContext: commentContext.ToString(), + userId: userId, + parentId: parentComment.Id, + textContent: newReplyText + ); + + var response = await CommentsService.CreateCommentAsync(command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Reply added successfully!", Severity.Success); + newReplyText = string.Empty; + await LoadPostsAsync(); + } + else + { + Snackbar.Add($"Failed to add reply: {response.ErrorMessage?.Reason}", Severity.Error); + } + } + + private async Task AddLikeToCommentAsync(CommentDto comment, PostDto post) + { + var command = new AddLikeDto(comment.Id, currentUserId.Value, DetermineCommentContext(post).ToString()); + + var response = await CommentsService.AddLikeAsync(command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Comment liked successfully!", Severity.Success); + await LoadPostsAsync(); + } + else + { + Snackbar.Add($"Failed to like comment: {response.ErrorMessage?.Reason}", Severity.Error); + } + } + + private async Task AddLikeToReplyAsync(ReplyDto reply, PostDto post) + { + var command = new AddLikeDto(reply.Id, currentUserId.Value, DetermineCommentContext(post).ToString()); + + var response = await CommentsService.AddLikeAsync(command); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add("Reply liked successfully!", Severity.Success); + await LoadPostsAsync(); + } + else + { + Snackbar.Add($"Failed to like reply: {response.ErrorMessage?.Reason}", Severity.Error); + } + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Reports/Dialogs/CreateReportDialog.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Reports/Dialogs/CreateReportDialog.razor deleted file mode 100644 index 0c4f97bd6..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Reports/Dialogs/CreateReportDialog.razor +++ /dev/null @@ -1,82 +0,0 @@ -@page "/reports/create/dialog" -@using MiniSpace.Web.Areas.Reports -@using MiniSpace.Web.Models.Reports -@using Radzen -@inject DialogService DialogService -@inject IReportsService ReportsService - - - @errorMessage - - - - - - - - - - - - - - - - - - - - - - - - - - - -@code { - [Parameter] - public CreateReportModel CreateReportModel { get; set; } - - private bool showError = false; - private string errorMessage = string.Empty; - - private readonly List> categories = - [ - new KeyValuePair("Spam", "Spam"), - new KeyValuePair("Harassment and bullying", "HarassmentAndBullying"), - new KeyValuePair("Violence", "Violence"), - new KeyValuePair("Sexual content", "SexualContent"), - new KeyValuePair("Misinformation", "Misinformation"), - new KeyValuePair("Privacy violations", "PrivacyViolations"), - new KeyValuePair("Intellectual property violations", "IntellectualPropertyViolations"), - new KeyValuePair("Other violations", "OtherViolations") - ]; - - protected override async Task OnInitializedAsync() - { - await base.OnInitializedAsync(); - } - - private async Task CreateReport(CreateReportModel createReportModel) - { - var response = await ReportsService.CreateReportAsync(Guid.Empty, createReportModel.IssuerId, - createReportModel.TargetId, createReportModel.TargetOwnerId, createReportModel.ContextType, - createReportModel.Category, CreateReportModel.Reason); - - if (response.ErrorMessage != null) - { - showError = true; - errorMessage = $"Error during reporting: {response.ErrorMessage.Reason}"; - } - else - { - DialogService.Close(true); - } - } -} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml b/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml index 68b313dff..4f1f53da1 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/_Host.cshtml @@ -219,10 +219,96 @@ }); } + + function initializeCommunicationSignalR() { + if (!userId) return; + + const communicationConnection = new signalR.HubConnectionBuilder() + .withUrl(`http://localhost:5016/chatHub?userId=${userId}`) + .withAutomaticReconnect([0, 2000, 10000, 30000]) // Retry delays + .build(); + + communicationConnection.on("ReceiveMessage", function (jsonMessage) { + console.log("Message received: ", jsonMessage); + DotNet.invokeMethodAsync('MiniSpace.Web', 'ReceiveMessage', jsonMessage); + }); + + communicationConnection.start().then(function () { + console.log("Communication SignalR connection established."); + }).catch(function (err) { + console.error("Error establishing SignalR connection: ", err.toString()); + }); + + communicationConnection.onreconnecting((error) => { + console.warn(`Connection lost due to error "${error}". Reconnecting.`); + }); + + communicationConnection.onreconnected((connectionId) => { + console.log(`Connection reestablished. Connected with connectionId "${connectionId}".`); + }); + + communicationConnection.onclose((error) => { + console.error(`Connection closed due to error "${error}". Attempting to reconnect...`); + setTimeout(() => initializeCommunicationSignalR(), 5000); + }); + } + + window.getSystemTheme = () => { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }; + window.scrollToBottom = function (elementId) { + var element = document.getElementById(elementId); + if (element) { + element.scrollTop = element.scrollHeight; + } + }; + + window.initializeInfiniteScroll = (element) => { + if (!element || !(element instanceof Element)) { + console.warn('Element for infinite scroll is not defined or is not a valid DOM element.'); + return; + } + + const observer = new IntersectionObserver(entries => { + entries.forEach(entry => { + if (entry.isIntersecting) { + // Call Blazor method to load more comments + DotNet.invokeMethodAsync('MiniSpace.Web', 'LoadMoreCommentsAsync') + .catch(err => console.error('Error invoking Blazor method:', err)); + } + }); + }, { + threshold: 1.0 + }); + + // Disconnect any existing observer to avoid multiple triggers + const lastChild = element.lastElementChild; + if (lastChild) { + observer.observe(lastChild); + } +}; + +window.getDeviceType = () => { + const ua = navigator.userAgent; + if (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(ua)) { + return "Mobile"; + } + if (/tablet|ipad/i.test(ua)) { + return "Tablet"; + } + return "Desktop"; +}; + + + + + + + + + @@ -230,7 +316,6 @@ - diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/AuthenticatedLayout.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/AuthenticatedLayout.razor deleted file mode 100644 index 09940b9df..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Shared/AuthenticatedLayout.razor +++ /dev/null @@ -1,79 +0,0 @@ -@using Microsoft.AspNetCore.Components.Authorization -@using Radzen -@using MiniSpace.Web.Areas.Students -@inherits LayoutComponentBase -@inject IIdentityService IdentityService -@inject IStudentsService StudentsService -@inject NavigationManager NavigationManager -@inject Blazored.LocalStorage.ILocalStorageService localStorage - - - - - - - - -

Sorry, there's nothing at this address.

-
-
-
-
- - -
- - - @if (IsUserAuthenticated() && StudentsService.StudentDto.State == "valid") - { - - } -
- -
-
- - -
- -
-
-
-
-
- @if (IsUserAuthenticated() && StudentsService.StudentDto.State == "valid") - { - - - - - - - - - } - -
- @Body -
-
-
- -
- - -@code { - bool _sidebarExpanded = true; - - public bool IsUserAuthenticated() => IdentityService.IsAuthenticated; - - void NavigateToHome() { - NavigationManager.NavigateTo("/home"); - } - - async Task SignOut() { - await localStorage.RemoveItemAsync("accessToken"); - await localStorage.RemoveItemAsync("jwtDto"); - NavigationManager.NavigateTo("signin", forceLoad: true); - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/FooterComponent.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/FooterComponent.razor index 3441d353f..07136f38d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Shared/FooterComponent.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/FooterComponent.razor @@ -3,6 +3,7 @@ @inherits LayoutComponentBase + + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor index 3c4385695..6fe90ad3c 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Shared/MainLayout.razor @@ -47,8 +47,8 @@
- Profile - Settings + Profile + Settings Account settings Notifications Sign Out @@ -118,11 +118,18 @@ Organizations I Follow + + All Chats + New Chat + Chat History + + All New History + Reports @if (IdentityService.GetCurrentUserRole() == "admin") { @@ -138,20 +145,20 @@
- - - @Body + + @Body - @if (_isUserAuthenticated) - { -
- -
- } + @if (_isUserAuthenticated) + { +
+ +
+ } -
+
+ @@ -259,7 +266,7 @@ { if (NavigationManager.Uri != NavigationManager.BaseUri) { - NavigationManager.NavigateTo("/", true); + NavigationManager.NavigateTo("/home", true); } while (NavigationManager.Uri != NavigationManager.BaseUri) { @@ -270,7 +277,7 @@ void NavigateToHome() { - NavigationManager.NavigateTo("/"); + NavigationManager.NavigateTo("/home"); } private string _themeName = "light"; diff --git a/MiniSpace.Web/src/MiniSpace.Web/Shared/NotAuthenticatedLayout.razor b/MiniSpace.Web/src/MiniSpace.Web/Shared/NotAuthenticatedLayout.razor deleted file mode 100644 index 34f8a1255..000000000 --- a/MiniSpace.Web/src/MiniSpace.Web/Shared/NotAuthenticatedLayout.razor +++ /dev/null @@ -1,50 +0,0 @@ -@using Radzen -@using MiniSpace.Web.Areas.Students -@inherits LayoutComponentBase -@inject IIdentityService IdentityService -@inject NavigationManager NavigationManager -@inject Microsoft.JSInterop.IJSRuntime JSRuntime - - -
- - -
- -
-
- -
- - - - - -
-
-
-
- -
- @Body -
-
- -
- - -@code { - void NavigateToHome() { - NavigationManager.NavigateTo("/"); - } - - async Task ScrollToSection(string sectionId) { - if (NavigationManager.Uri != NavigationManager.BaseUri) { - NavigationManager.NavigateTo("/", true); - } - while (NavigationManager.Uri != NavigationManager.BaseUri) { - await Task.Delay(100); - } - await JSRuntime.InvokeVoidAsync("scrollToSection", sectionId); - } -} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Startup.cs b/MiniSpace.Web/src/MiniSpace.Web/Startup.cs index da3ef5f0e..a96fd4261 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Startup.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Startup.cs @@ -29,6 +29,7 @@ using MiniSpace.Web.Areas.Reactions; using MiniSpace.Web.Areas.Reports; using Microsoft.AspNetCore.Server.Kestrel.Core; +using MiniSpace.Web.Areas.Communication; namespace MiniSpace.Web { @@ -105,9 +106,11 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } diff --git a/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor b/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor index c98b4e2a4..dc6414380 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/_Imports.razor @@ -16,17 +16,39 @@ @using MiniSpace.Web.Areas.MediaFiles @using MiniSpace.Web.Areas.Organizations -@using Radzen.Blazor @using Cropper.Blazor.Components @using MiniSpace.Web.DTO +@using MiniSpace.Web.DTO.Enums @using MiniSpace.Web.DTO.Interests @using MiniSpace.Web.DTO.Languages @using MiniSpace.Web.DTO.Organizations +@using MiniSpace.Web.DTO.Wrappers @using MiniSpace.Web.Areas.Organizations.CommandsDto @using MiniSpace.Web.Areas.PagedResult @using MiniSpace.Web.Areas.Events @using MiniSpace.Web.DTO.Events @using MiniSpace.Web.Areas.Posts -@using MiniSpace.Web.Areas.Events.CommandsDto \ No newline at end of file +@using MiniSpace.Web.Areas.Events.CommandsDto + +@using MiniSpace.Web.DTO.Enums.Reactions +@using MiniSpace.Web.Areas.Reactions +@using MiniSpace.Web.Data.Posts +@using MiniSpace.Web.Areas.Reactions.CommandDto + +@using MiniSpace.Web.Areas.Comments +@using MiniSpace.Web.Areas.Comments.CommandsDto +@using MiniSpace.Web.DTO.Comments +@using MiniSpace.Web.Areas.Comments.CommandDto + +@using MiniSpace.Web.Areas.Friends.CommandsDto +@using MiniSpace.Web.Areas.Friends +@using MiniSpace.Web.DTO.Friends +@using MiniSpace.Web.DTO.States; + +@using MiniSpace.Web.Areas.Communication +@using MiniSpace.Web.DTO.Communication +@using MiniSpace.Web.Areas.Communication.CommandsDto + +@using MiniSpace.Web.DTO.Users \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css index 801a5014c..6c0eaf2f6 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css +++ b/MiniSpace.Web/src/MiniSpace.Web/wwwroot/css/site.css @@ -131,7 +131,7 @@ app { .main-container { display: flex; flex-direction: column; - height: 100vh; + min-height: 100vh; } /* header { @@ -907,4 +907,48 @@ div.connectionRejected { width: 100%; height: auto; border-radius: 8px; -} \ No newline at end of file +} + +.avatar-overlay { + bottom: 40px; + left: 16px; + border: 3px solid white; + box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.2); +} + + /* Mobile-specific styles */ + @media (max-width: 800px) { + .chat-name { + display: none !important; + } + } + @media (max-width: 600px) { + /* .chat-name { + display: none !important; + } */ + + .conversation-list-container { + width: 60px; + padding: 0; + } + + .conversation-list .MudListItemIcon { + margin-right: 0; + } + + .chat-container { + padding: 0 8px; + } +} + +.typing-indicator { + font-style: italic; + color: #555; + margin-bottom: 10px; +} + + +.mud-dialog-blur-backdrop { + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +}