diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index 60af690..932256c 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -7,7 +7,7 @@ - + diff --git a/AskFm/AskFm.API/Controllers/CommentController.cs b/AskFm/AskFm.API/Controllers/CommentController.cs index 21887c9..3af4fa5 100644 --- a/AskFm/AskFm.API/Controllers/CommentController.cs +++ b/AskFm/AskFm.API/Controllers/CommentController.cs @@ -13,13 +13,11 @@ namespace AskFm.API.Controllers; [Authorize(AuthenticationSchemes = "Bearer")] public class CommentController : ControllerBase { - private readonly ICommentLikeService _commentLikeService; private readonly ICommentService _commentService; private readonly ILogger _logger; private readonly IUserService _userService; - - + public CommentController( ICommentLikeService commentLikeService, ICommentService commentService, @@ -31,11 +29,7 @@ public CommentController( _commentService = commentService; _userService = userService; } - - - - - + // GET api/comment/{id}/likes -> get all the likes for a Comment with id = id [HttpGet("{id}/likes")] public async Task GetAllLikes(int id) @@ -55,7 +49,6 @@ public async Task GetAllLikes(int id) return NotFound(new { message = ex.Message, - }); } catch (Exception ex) @@ -65,9 +58,6 @@ public async Task GetAllLikes(int id) } } - - - // POST api/comment/{id}/likes -> add a like for a Comment with id = id [HttpPost("{id}/likes")] public async Task AddLike(int id) @@ -75,21 +65,21 @@ public async Task AddLike(int id) try { var user = await _userService.GetCurrentUserAsync(); - + if (!user.success) { return BadRequest(user.Errors); } - + var createdLike = await _commentLikeService.AddLikeAsync(id, user.Data.Id); - + if (!createdLike.success) { return BadRequest(createdLike.Errors); } return CreatedAtAction( - nameof(GetAllLikes), - new { id = id }, + nameof(GetAllLikes), + new { id = id }, createdLike.Data); } catch (ArgumentException ex) @@ -108,37 +98,31 @@ public async Task AddLike(int id) return StatusCode(500, new { message = "An error occurred while adding like" }); } } - - - - + [HttpDelete("{id}/likes")] public async Task DeleteLike(int id) { int userId = 0; try { - var user = await _userService.GetCurrentUserAsync(); - if (user==null || !user.success) + if (user == null || !user.success) return BadRequest(user.Errors); - - + userId = user.Data.Id; var comment = await _commentService.GetCommentAsync(id); - + if (comment == null || !user.success) return BadRequest(user.Errors); - - + var result = await _commentLikeService.DeleteLikeAsync(id, userId); if (!result.success) { return BadRequest(result.Errors); } - + return NoContent(); } catch (ArgumentException ex) @@ -157,5 +141,58 @@ public async Task DeleteLike(int id) return StatusCode(500, new { message = "An error occurred while deleting like" }); } } - + + // POST api/threads/{id}/comments - Add a comment to the thread with id = {id} + [HttpPost] + [Route("threads/{id}/comments")] + public async Task AddComment([FromRoute] int id, [FromBody] CreateCommentDto createCommentDto) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _commentService.AddComment(id, user.Data.Id, createCommentDto); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(result.Data); + } + + // GET api/threads/{id}/comments - Get all comments for the thread with id = {id} + [HttpGet] + [Route("threads/{id}/comments")] + public async Task GetComments([FromRoute] int id, [FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + var result = await _commentService.GetCommentsByThreadId(id, page, pageSize); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(result.Data); + } + + // DELETE api/threads/{threadId}/comments/{commentId} - Delete a comment with id = {commentId} + [HttpDelete] + [Route("threads/{threadId}/comments/{commentId}")] + public async Task DeleteComment([FromRoute] int threadId, [FromRoute] int commentId) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _commentService.DeleteComment(threadId, commentId, user.Data.Id); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(new { message = "Comment deleted successfully" }); + } } \ No newline at end of file diff --git a/AskFm/AskFm.API/Controllers/ThreadController.cs b/AskFm/AskFm.API/Controllers/ThreadController.cs new file mode 100644 index 0000000..d82796f --- /dev/null +++ b/AskFm/AskFm.API/Controllers/ThreadController.cs @@ -0,0 +1,213 @@ +using AskFm.BLL.DTO; +using AskFm.BLL.Services; +using AskFm.BLL.Services.UserIdentityService; +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; + +namespace AskFm.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize(AuthenticationSchemes = "Bearer")] +public class ThreadController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IUserService _userService; + private readonly IThreadService _threadService; + + public ThreadController( + ILogger logger, + IUserService userService, + IThreadService threadService) + { + _logger = logger; + _userService = userService; + _threadService = threadService; + } + + + // POST api/threads/ - Ask a question + [HttpPost] + [Route("thread")] + public async Task AskQuestion(CreateThreadDto createThreadDto) + { + // Asker Id -> Current User + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + var userId = user.Data.Id; + + var result = await _threadService.AddThread(userId, createThreadDto); + + if (!result.success) + { + return BadRequest(result.Errors); + } + return Ok(result.Data); + + } + + + // get all threads for user by user id + [HttpGet] + [Route("thread/{id}")] + public async Task GetAllThreads([FromRoute] int id) + { + var threads = await _threadService.GetAllThreads(id); + if (!threads.success) + { + return BadRequest(threads.Errors); + } + return Ok(threads.Data); + } + + // GET api/threads/{id} - Getting the Thread with id = {id} + [HttpGet] + [Route("threads/{id}")] + public async Task GetThreadWithId([FromRoute] int id) + { + var thread = await _threadService.GetThreadById(id); + if (!thread.success) + { + return BadRequest(thread.Errors); + } + return Ok(thread.Data); + } + + + // PUT api/threads/{id}/answer - Add an Answer on the thread with id = {id} + [HttpPut] + [Route("threads/{id}/answer")] + public async Task AnswerQuestion([FromRoute] int id, [FromBody] AnswerThreadDto answerDto) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _threadService.AnswerThread(id, user.Data.Id, answerDto); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(result.Data); + } + + // GET api/threads - Get all threads (with pagination) + [HttpGet] + [Route("threads")] + public async Task GetThreads([FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + var threads = await _threadService.GetThreads(page, pageSize); + if (!threads.success) + { + return BadRequest(threads.Errors); + } + return Ok(threads.Data); + } + + // DELETE api/threads/{id} - Delete a thread + [HttpDelete] + [Route("threads/{id}")] + public async Task DeleteThread([FromRoute] int id) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _threadService.DeleteThread(id, user.Data.Id); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(new { message = "Thread deleted successfully" }); + } + + // GET api/threads/feed - Get personalized feed for current user + [HttpGet] + [Route("threads/feed")] + public async Task GetFeed([FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var feed = await _threadService.GetFeed(user.Data.Id, page, pageSize); + if (!feed.success) + { + return BadRequest(feed.Errors); + } + + return Ok(feed.Data); + } + + // POST api/threads/{id}/save - Save a thread + [HttpPost] + [Route("threads/{id}/save")] + public async Task SaveThread([FromRoute] int id) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _threadService.SaveThread(id, user.Data.Id); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(new { message = "Thread saved successfully" }); + } + + // DELETE api/threads/{id}/save - Unsave a thread + [HttpDelete] + [Route("threads/{id}/save")] + public async Task UnsaveThread([FromRoute] int id) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _threadService.UnsaveThread(id, user.Data.Id); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(new { message = "Thread unsaved successfully" }); + } + + // GET api/threads/saved - Get all saved threads for current user + [HttpGet] + [Route("threads/saved")] + public async Task GetSavedThreads([FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var threads = await _threadService.GetSavedThreads(user.Data.Id, page, pageSize); + if (!threads.success) + { + return BadRequest(threads.Errors); + } + + return Ok(threads.Data); + } +} \ No newline at end of file diff --git a/AskFm/AskFm.API/Controllers/ThreadLikeController.cs b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs new file mode 100644 index 0000000..e861388 --- /dev/null +++ b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs @@ -0,0 +1,82 @@ +using AskFm.BLL.DTO; +using AskFm.BLL.Services; +using AskFm.BLL.Services.UserIdentityService; +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; + +namespace AskFm.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize(AuthenticationSchemes = "Bearer")] +public class ThreadLikeController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IUserService _userService; + private readonly IThreadLikeService _threadLikeService; + + public ThreadLikeController( + ILogger logger, + IUserService userService, + IThreadLikeService threadService) + { + _logger = logger; + _userService = userService; + _threadLikeService = threadService; + } + + + // POST api/threads/{id}/likes - Add a Like to the thread with id = {id} + [HttpPost] + [Route("threads/{id}/likes")] + public async Task LikeThread([FromRoute] int id) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + var res = await _threadLikeService.AddLike(id, user.Data.Id); + if (!res.success) + { + return BadRequest(res.Errors); + } + return Ok(res.Data); + + } + + + // GET api/threads/{id}/likes - Get all the Likes to the thread with id = {id} + [HttpGet] + [Route("threads/{id}/likes")] + public async Task GetLikes([FromRoute] int id) + { + var likes = await _threadLikeService.GetLikes(id); + if (!likes.success) + { + return BadRequest(likes.Errors); + } + return Ok(likes.Data); + } + + // DELETE api/threads/{id}/likes - Unlike to the thread with id = {id} + [HttpDelete] + [Route("threads/{id}/likes")] + public async Task UnlikeThread([FromRoute] int id) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var res = await _threadLikeService.RemoveLike(id, user.Data.Id); + if (!res.success) + { + return BadRequest(res.Errors); + } + + return Ok(new { message = "Thread unliked successfully" }); + } +} \ No newline at end of file diff --git a/AskFm/AskFm.API/Controllers/WeatherForecastController.cs b/AskFm/AskFm.API/Controllers/WeatherForecastController.cs index a2f0751..bf50c5f 100644 --- a/AskFm/AskFm.API/Controllers/WeatherForecastController.cs +++ b/AskFm/AskFm.API/Controllers/WeatherForecastController.cs @@ -23,7 +23,7 @@ public IEnumerable Get() { return Enumerable.Range(1, 5).Select(index => new WeatherForecast { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Date = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }) diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 9bf0d5a..6d7c379 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -50,12 +50,15 @@ public static void Main(string[] args) .UseSqlServer(ConnectionString)); // ------------------------------------------------- // Register the repositories and services + builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index bc89689..5bfac97 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -9,9 +9,8 @@ + - - diff --git a/AskFm/AskFm.BLL/DTO/AnswerThreadDto.cs b/AskFm/AskFm.BLL/DTO/AnswerThreadDto.cs new file mode 100644 index 0000000..b6a118e --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/AnswerThreadDto.cs @@ -0,0 +1,6 @@ +namespace AskFm.BLL.DTO; + +public class AnswerThreadDto +{ + public string AnswerContent { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs b/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs new file mode 100644 index 0000000..f19df0a --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs @@ -0,0 +1,12 @@ +namespace AskFm.BLL.DTO; + +public class CommentResponseDto +{ + public int Id { get; set; } + public string Content { get; set; } + public int? UserId { get; set; } + public string UserName { get; set; } + public int ThreadId { get; set; } + public DateTime CreatedAt { get; set; } + public int LikesCount { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/CreateCommentDto.cs b/AskFm/AskFm.BLL/DTO/CreateCommentDto.cs new file mode 100644 index 0000000..51d8ae3 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/CreateCommentDto.cs @@ -0,0 +1,6 @@ +namespace AskFm.BLL.DTO; + +public class CreateCommentDto +{ + public string Content { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs b/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs new file mode 100644 index 0000000..998c944 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using AskFm.DAL.Enums; +using AskFm.DAL.Models; + +namespace AskFm.BLL.DTO; + +public class CreateThreadDto +{ + [Required(ErrorMessage = "Asked user ID is required")] + public int AskedId { get; set; } + + [Required(ErrorMessage = "Question content is required")] + [StringLength(1000, MinimumLength = 2, ErrorMessage = "Question must be between 2 and 1000 characters")] + public string QuestionContent { get; set; } + + public ThreadStatus Status { get; set; } + + public bool IsAnonymous { get; set; } + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs b/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs new file mode 100644 index 0000000..e6f0636 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs @@ -0,0 +1,9 @@ +namespace AskFm.BLL.DTO; + +public class PagedResponseDto +{ + public List Items { get; set; } + public int PageNumber { get; set; } + public int PageSize { get; set; } + public bool HasMore { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs b/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs index cf524b2..68a00a1 100644 --- a/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs +++ b/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs @@ -5,7 +5,7 @@ public class RefreshTokenDto { public string Token { get; set; } public DateTime ExpireOn { get; set; } - public bool IsExpired => DateTime.Now >= ExpireOn; + public bool IsExpired => DateTime.UtcNow >= ExpireOn; public int ExpireAfter { get; set; } public DateTime CreatedOn { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs b/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs new file mode 100644 index 0000000..dc0be27 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs @@ -0,0 +1,12 @@ +namespace AskFm.BLL.DTO; + +public class ThreadLikeResponseDto +{ + public int UserId {get;set;} + public int ThreadId {get;set;} + public DateTime CreatedAt {get;set;} + public string UserName {get;set;} + public string ProfilePicture {get;set;} + + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs new file mode 100644 index 0000000..c713574 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs @@ -0,0 +1,20 @@ +using AskFm.DAL.Enums; + +namespace AskFm.BLL.DTO; + +public class ThreadResponseDto +{ + public int Id { get; set; } + public string QuestionContent { get; set; } + public string? AnswerContent { get; set; } + public ThreadStatus Status { get; set; } + public bool IsAnonymous { get; set; } + public DateTime CreatedAt { get; set; } + public int? AskerId { get; set; } + public string? AskerName { get; set; } + public int AskedId { get; set; } + public string? AskedName { get; set; } + public int LikesCount { get; set; } + public int CommentsCount { get; set; } + public DateTime? SavedAt { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/CommentLikeService.cs b/AskFm/AskFm.BLL/Services/CommentLikeService.cs index 2033711..27d0a09 100644 --- a/AskFm/AskFm.BLL/Services/CommentLikeService.cs +++ b/AskFm/AskFm.BLL/Services/CommentLikeService.cs @@ -4,7 +4,6 @@ using AskFm.DAL.Models; using AutoMapper; using Microsoft.Extensions.Logging; -using Microsoft.VisualBasic; namespace AskFm.BLL.Services; @@ -111,6 +110,7 @@ public async Task> AddLikeAsync(int commentId, int // otherwise , the user liked the comment , then unliked it , and then wants to like it again existingLike.IsDeleted = false; comment.LikeCount++; + existingLike.CreatedAt = DateTime.UtcNow; _unitOfWork.Comments.Update(comment); await _unitOfWork.SaveAsync(); await transaction.CommitAsync(); @@ -119,7 +119,6 @@ public async Task> AddLikeAsync(int commentId, int // updating the createdAt column to Now , ignoring the first time the user liked the comment - existingLike.CreatedAt = DateTime.Now; return await ServiceResult.Success(new CommentLikeDto { CommentId = existingLike.CommentId, diff --git a/AskFm/AskFm.BLL/Services/CommentService.cs b/AskFm/AskFm.BLL/Services/CommentService.cs index 81f7c98..6400a88 100644 --- a/AskFm/AskFm.BLL/Services/CommentService.cs +++ b/AskFm/AskFm.BLL/Services/CommentService.cs @@ -1,28 +1,231 @@ +using AskFm.BLL.DTO; using AskFm.DAL.Interfaces; using AskFm.DAL.Models; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace AskFm.BLL.Services; public class CommentService : ICommentService { - private IUnitOfWork _unitOfWork; - public CommentService(IUnitOfWork unitOfWork) + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public CommentService(IUnitOfWork unitOfWork, ILogger logger) { _unitOfWork = unitOfWork; + _logger = logger; } - - public async Task> GetCommentAsync(int commentId) + + public async Task> GetCommentAsync(int id) { try { - var comment = await _unitOfWork.Comments.GetByIdAsync(commentId); - return await ServiceResult.Success(comment); + var comment = await _unitOfWork.Comments.FindAsync( + c => c.Id == id, + new[] { "User", "CommentLikes" } + ); + + if (comment == null) + { + return await ServiceResult.Failure(new List() { "Comment not found" }); + } + + var commentDto = new CommentResponseDto + { + Id = comment.Id, + Content = comment.Content, + UserId = comment.UserId, + UserName = comment.User?.Name ?? "Unknown", + ThreadId = comment.ThreadId, + CreatedAt = comment.CreatedAt, + LikesCount = comment.CommentLikes?.Count ?? 0, + }; + + return await ServiceResult.Success(commentDto); } catch (Exception e) { - return await ServiceResult.Failure(new List(){e.Message}); + _logger.LogError(e, "Error retrieving comment with ID {Id}", id); + return await ServiceResult.Failure(new List() { e.Message }); } } - + public async Task> AddComment(int threadId, int userId, CreateCommentDto commentDto) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // Check if thread exists + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == threadId); + if (thread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + // Check if user exists + var user = await _unitOfWork.Users.FindAsync(u => u.Id == userId); + if (user == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "User not found" }); + } + + // Validate comment content + if (string.IsNullOrWhiteSpace(commentDto.Content)) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Comment content cannot be empty" }); + } + + // Create comment + var comment = new Comment + { + Content = commentDto.Content, + UserId = userId, + ThreadId = threadId, + CreatedAt = DateTime.UtcNow, + CommentLikes = new List() + }; + + // Add comment to database + await _unitOfWork.Comments.AddAsync(comment); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + // Return response dto + var commentResponseDto = new CommentResponseDto + { + Id = comment.Id, + Content = comment.Content, + UserId = comment.UserId, + UserName = user.Name, + ThreadId = comment.ThreadId, + CreatedAt = comment.CreatedAt, + LikesCount = 0, + }; + + return await ServiceResult.Success(commentResponseDto); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error adding comment to thread {ThreadId}", threadId); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetCommentsByThreadId(int threadId, int page, int pageSize) + { + try + { + // Check if thread exists + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == threadId); + if (thread == null) + { + return await ServiceResult>.Failure(new List() { "Thread not found" }); + } + + + int skip = (page - 1) * pageSize; + // Get paginated comments + var comments = await _unitOfWork.Comments.GetPagedAsync( + skip: skip, + take: pageSize + 1, + orderBy: c => c.CreatedAt, + ascending: false, // Newest first + predicate: c => c.ThreadId == threadId, + includes: new[] { "User", "CommentLikes" } + ); + + bool hasMore = comments.Count > pageSize; + + var trimmed = comments.Take(pageSize).ToList(); + + var commentDtos = trimmed.Select(c => new CommentResponseDto + { + Id = c.Id, + Content = c.Content, + UserId = c.UserId, + UserName = c.User?.Name ?? "Unknown", + ThreadId = c.ThreadId, + CreatedAt = c.CreatedAt, + LikesCount = c.CommentLikes?.Count ?? 0, + }).ToList(); + + // Create paged response + var response = new PagedResponseDto + { + Items = commentDtos, + PageNumber = page, + PageSize = pageSize, + HasMore = hasMore + }; + + return await ServiceResult>.Success(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving comments for thread {ThreadId}", threadId); + return await ServiceResult>.Failure(new List() { e.Message }); + } + } + + public async Task> DeleteComment(int threadId, int commentId, int userId) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // Check if comment exists + var comment = await _unitOfWork.Comments.FindAsync( + c => c.Id == commentId && c.ThreadId == threadId, + new[] { "CommentLikes" } + ); + + if (comment == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Comment not found" }); + } + + // Check if user is authorized to delete (either comment owner or thread owner) + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == threadId); + + if (comment.UserId != userId && thread.AskedId != userId && thread.AskerId != userId) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "User not authorized to delete this comment" }); + } + + // Remove comment likes first + if (comment.CommentLikes != null && comment.CommentLikes.Any()) + { + foreach (var like in comment.CommentLikes.ToList()) + { + await _unitOfWork.CommentLikes.RemoveAsync(like); + } + } + + // Remove comment + await _unitOfWork.Comments.RemoveAsync(comment); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + return await ServiceResult.Success(true); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error deleting comment {CommentId} from thread {ThreadId}", commentId, threadId); + return await ServiceResult.Failure(new List() { e.Message }); + } + } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ICommentService.cs b/AskFm/AskFm.BLL/Services/ICommentService.cs index dda8314..8c58275 100644 --- a/AskFm/AskFm.BLL/Services/ICommentService.cs +++ b/AskFm/AskFm.BLL/Services/ICommentService.cs @@ -1,8 +1,19 @@ +using AskFm.BLL.DTO; using AskFm.DAL.Models; namespace AskFm.BLL.Services; public interface ICommentService { - Task> GetCommentAsync(int commentId); + // Get a specific comment by ID + Task> GetCommentAsync(int id); + + // Add a comment to a thread + Task> AddComment(int threadId, int userId, CreateCommentDto commentDto); + + // Get all comments for a thread with pagination + Task>> GetCommentsByThreadId(int threadId, int page, int pageSize); + + // Delete a specific comment + Task> DeleteComment(int threadId, int commentId, int userId); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/IThreadLikeService.cs b/AskFm/AskFm.BLL/Services/IThreadLikeService.cs new file mode 100644 index 0000000..156e8d8 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/IThreadLikeService.cs @@ -0,0 +1,11 @@ +using AskFm.BLL.DTO; +using Microsoft.AspNetCore.Mvc; + +namespace AskFm.BLL.Services; + +public interface IThreadLikeService +{ + public Task> AddLike(int id, int userId); + public Task>> GetLikes(int id); + public Task> RemoveLike(int threadId, int userId); +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/IThreadService.cs b/AskFm/AskFm.BLL/Services/IThreadService.cs new file mode 100644 index 0000000..d329412 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/IThreadService.cs @@ -0,0 +1,17 @@ +using AskFm.BLL.DTO; + +namespace AskFm.BLL.Services; + +public interface IThreadService +{ + public Task> AddThread(int askerId, CreateThreadDto createThreadDto); + public Task> GetThreadById(int id); + public Task>> GetAllThreads(int askedId); + public Task> AnswerThread(int threadId, int userId, AnswerThreadDto answerDto); + public Task>> GetThreads(int page, int pageSize); + public Task> DeleteThread(int threadId, int userId); + public Task>> GetFeed(int userId, int page, int pageSize); + public Task> SaveThread(int threadId, int userId); + public Task> UnsaveThread(int threadId, int userId); + public Task>> GetSavedThreads(int userId, int page, int pageSize); +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ThreadLikeService.cs b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs new file mode 100644 index 0000000..27facb4 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs @@ -0,0 +1,203 @@ +using AskFm.BLL.DTO; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace AskFm.BLL.Services; + +public class ThreadLikeService : IThreadLikeService +{ + private IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + private readonly IMapper _mapper; + + public ThreadLikeService(IUnitOfWork unitOfWork, ILogger logger, IMapper mapper) + { + _unitOfWork = unitOfWork; + _logger = logger; + _mapper = mapper; + } + + // add a like on the Thread that has id = id + public async Task> AddLike(int id, int userId) + { + try + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + // get the thread, Including the ThreadLike Collection + var thread = await _unitOfWork.Threads.FindAsync( + predicate: thread => thread.Id == id, + includes: new[] { "ThreadLikes" } + ); + + // check if thread exists + if (thread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + // check if user has already liked this thread + var existingLike = thread.ThreadLikes?.FirstOrDefault(like => like.UserId == userId); + if (existingLike != null) + { + // if the The use already liked this thread + if (!existingLike.IsDeleted) + { + var errors = new List() + { + "User has already liked this thread" + }; + await transaction.RollbackAsync(); + return await ServiceResult.Failure(errors); + } + + // otherwise , the user liked the comment , then unliked it , and then wants to like it again + existingLike.IsDeleted = false; + existingLike.CreatedAt = DateTime.UtcNow; + thread.ThreadLikes.Add(existingLike); + _unitOfWork.Threads.Update(thread); + await _unitOfWork.SaveAsync(); + await transaction.CommitAsync(); + + _logger.LogInformation("Like added successfully for thread id: {threadId}", thread.Id); + + + // updating the createdAt column to Now , ignoring the first time the user liked the comment + return await ServiceResult.Success(new ThreadLikeResponseDto() + { + ThreadId = existingLike.ThreadId, + UserId = existingLike.UserId, + CreatedAt = existingLike.CreatedAt + }); + } + + + try + { + // create a new ThreadLike + var threadLike = new ThreadLike + { + ThreadId = id, + UserId = userId, + CreatedAt = DateTime.UtcNow + }; + + // add the ThreadLike to the Thread + thread.ThreadLikes?.Add(threadLike); + + // update the thread + await _unitOfWork.Threads.UpdateAsync(thread); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + // create and return the response DTO + var response = new ThreadLikeResponseDto + { + ThreadId = threadLike.ThreadId, + UserId = threadLike.UserId, + CreatedAt = threadLike.CreatedAt + }; + + return await ServiceResult.Success(response); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + throw; + } + } + catch (Exception e) + { + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetLikes(int id) + { + try + { + // get the thread, Including the ThreadLikes collection + var thread = await _unitOfWork.Threads.FindAsync( + predicate: thread => thread.Id == id, + includes: new[] { "ThreadLikes.User" } + ); + + // check if thread exists + if (thread == null) + { + return await ServiceResult>.Failure(new List() + { "Thread not found" }); + } + + // return the list of likes + var likes = thread.ThreadLikes.Select(like => new ThreadLikeResponseDto + { + ThreadId = like.ThreadId, + UserId = like.UserId, + UserName = like.User?.Name ?? "Unknown", + CreatedAt = like.CreatedAt + }).ToList(); + + return await ServiceResult>.Success(likes); + } + catch (Exception e) + { + return await ServiceResult>.Failure(new List() { e.Message }); + } + } + + // Remove a like from the thread with id = threadId + public async Task> RemoveLike(int threadId, int userId) + { + try + { + // get the thread with its likes + var thread = await _unitOfWork.Threads.FindAsync( + predicate: thread => thread.Id == threadId, + includes: new[] { "ThreadLikes" } + ); + + // check if thread exists + if (thread == null) + { + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + // find the like to remove + var likeToRemove = thread.ThreadLikes?.FirstOrDefault(like => like.UserId == userId); + if (likeToRemove == null) + { + return await ServiceResult.Failure(new List() { "Like not found for this user" }); + } + + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // remove the like from the thread + thread.ThreadLikes?.Remove(likeToRemove); + + // update the thread + await _unitOfWork.Threads.UpdateAsync(thread); + await _unitOfWork.SaveAsync(); + + transaction.Commit(); + + return await ServiceResult.Success(true); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + throw; + } + } + catch (Exception e) + { + return await ServiceResult.Failure(new List() { e.Message }); + } + } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ThreadService.cs b/AskFm/AskFm.BLL/Services/ThreadService.cs new file mode 100644 index 0000000..77cb2a7 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/ThreadService.cs @@ -0,0 +1,570 @@ +using AskFm.BLL.DTO; +using AskFm.DAL.Enums; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using AutoMapper; +using Microsoft.Extensions.Logging; +using Thread = AskFm.DAL.Models.Thread; + +namespace AskFm.BLL.Services; + +public class ThreadService : IThreadService +{ + private IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + private readonly IMapper _mapper; + + public ThreadService(IUnitOfWork unitOfWork, ILogger logger, IMapper mapper) + { + _unitOfWork = unitOfWork; + _logger = logger; + _mapper = mapper; + } + + public async Task> AddThread(int askerId, CreateThreadDto createThreadDto) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // check if the Asked user exite or not + var askedId = createThreadDto.AskedId; + + var askedUser = await _unitOfWork.Users.GetByIdAsync(askedId); + if (askedUser == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure( + new List() { "Could not find asked user" }); + } + + // getting the Asker user object + var askerUser = await _unitOfWork.Users.GetByIdAsync(askerId); + if (askerUser == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure( + new List() { "Could not find asker user" }); + } + + + // check if the Asked User's id == Asker User's Id + if (askedId == askerId) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure( + new List() { "User can not ask him self" }); + } + + + // check if the QuestionContent is empty or not + if (string.IsNullOrEmpty(createThreadDto.QuestionContent)) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() + { "Question Can't be null or empty" }); + } + + // creating a new thread + Thread thread = new Thread + { + AskedId = askedId, + + AskerId = askerId, + + QuestionContent = createThreadDto.QuestionContent, + + AnswerContent = "", + Status = createThreadDto.Status, + isAnonymous = createThreadDto.IsAnonymous, + CreatedAt = DateTime.UtcNow, + }; + + await _unitOfWork.Threads.AddAsync(thread); + askerUser.AskedThreads?.Add(thread); + askedUser.ReceivedThreads?.Add(thread); + await _unitOfWork.Users.UpdateAsync(askedUser); + await _unitOfWork.Users.UpdateAsync(askerUser); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + var responseDto = new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId.Value, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, + AskedId = thread.AskedId, + AskedName = askedUser.Name + }; + + return await ServiceResult.Success(responseDto); + } + catch (Exception e) + { + _logger.LogError(e, "Error adding thread"); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task> GetThreadById(int id) + { + try + { + var thread = await _unitOfWork.Threads.FindAsync( + predicate: t => t.Id == id && !t.IsDeleted, + includes: new[] { "Asker", "Asked", "Comments", "ThreadLikes" } + ); + + if (thread == null) + { + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + var threadDto = new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + AnswerContent = thread.AnswerContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId ?? 0, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, + AskedId = thread.AskedId, + AskedName = thread.Asked?.Name, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0 + }; + + return await ServiceResult.Success(threadDto); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving thread"); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetAllThreads(int askedId) + { + try + { + // Get all threads for user + var threads = await _unitOfWork.Threads.FindAllAsync( + predicate: t => t.AskedId == askedId, + includes: new[] { "Asker", "Asked", "Comments", "ThreadLikes" } + ); + + if (threads == null || !threads.Any()) + { + return await ServiceResult>.Success(new List()); + } + + var threadDtos = threads.Select(thread => new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + AnswerContent = thread.AnswerContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId ?? 0, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, + AskedId = thread.AskedId, + AskedName = thread.Asked?.Name, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0 + }).ToList(); + + return await ServiceResult>.Success(threadDtos); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving threads"); + return await ServiceResult>.Failure(new List() { e.Message }); + } + } + + public async Task> AnswerThread(int threadId, int userId, + AnswerThreadDto answerDto) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + if (string.IsNullOrWhiteSpace(answerDto.AnswerContent)) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure( + new List { "Answer cannot be empty or contain only whitespace" }); + } + + var trimmedAnswer = answerDto.AnswerContent.Trim(); + if (trimmedAnswer.Length > 5000) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure( + new List { "Answer cannot exceed 5000 characters" }); + } + + var thread = await _unitOfWork.Threads.FindAsync( + predicate: t => t.Id == threadId, + includes: new[] { "Asker", "Asked" } + ); + + if (thread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + if (thread.AskedId != userId) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() + { "User not authorized to answer this thread" }); + } + + thread.AnswerContent = answerDto.AnswerContent; + thread.Status = ThreadStatus.Answered; + + await _unitOfWork.Threads.UpdateAsync(thread); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + var threadDto = new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + AnswerContent = thread.AnswerContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId ?? 0, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, + AskedId = thread.AskedId, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0, + AskedName = thread.Asked?.Name + }; + + return await ServiceResult.Success(threadDto); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error answering thread"); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetThreads(int page, int pageSize) + { + try + { + int skipCount = (page - 1) * pageSize; + + // var totalCount = await _unitOfWork.Threads.CountAsync(); + + var threads = await _unitOfWork.Threads.GetPagedAsync( + skipCount, + pageSize + 1, + t => t.CreatedAt, + false, + t => true, + new[] { "Asker", "Asked", "Comments", "ThreadLikes" } + ); + + bool hasMore = threads.Count > pageSize; + + var trimmed = threads.Take(pageSize).ToList(); + + var threadDtos = trimmed.Select(thread => new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + AnswerContent = thread.AnswerContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId ?? 0, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, + AskedId = thread.AskedId, + AskedName = thread.Asked?.Name, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0 + }).ToList(); + + var response = new PagedResponseDto + { + Items = threadDtos, + PageNumber = page, + PageSize = pageSize, + HasMore = hasMore + + }; + + return await ServiceResult>.Success(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving threads"); + return await ServiceResult>.Failure(new List() { e.Message }); + } + } + + public async Task> DeleteThread(int threadId, int userId) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // Get the thread + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == threadId); + + if (thread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + // Check if user is authorized to delete this thread (either asker or asked) + if (thread.AskerId != userId && thread.AskedId != userId) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() + { "User not authorized to delete this thread" }); + } + + // Delete the thread + await _unitOfWork.Threads.RemoveAsync(thread); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + return await ServiceResult.Success(true); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error deleting thread"); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetFeed(int userId, int page, int pageSize) + { + try + { + int skipCount = (page - 1) * pageSize; + + var followedUsers = await _unitOfWork.Follows.FindAllAsync(f => f.FollowerId == userId); + var followedUserIds = followedUsers.Select(f => f.FollowedId).ToList(); + + followedUserIds.Add(userId); + + var totalCount = await _unitOfWork.Threads.CountAsync(t => + followedUserIds.Contains(t.AskedId) && t.Status == ThreadStatus.Answered); + + var threads = await _unitOfWork.Threads.GetPagedAsync( + skipCount, + pageSize + 1, + t => t.CreatedAt, + false, + t => followedUserIds.Contains(t.AskedId) && t.Status == ThreadStatus.Answered, + new[] { "Asker", "Asked", "Comments", "ThreadLikes" } + ); + + // asking if still there is some remaining pages withou using COUNT() + bool hasMore = threads.Count > pageSize; + + /* after getting the threads with size of pageSize + 1 + to find if there are remaining threads (nextPage) without usint TotalCound and totalPages + now we need to return back only the PageSize of threads + that what i'm doing here in the trimmed list, + */ + var trimmed = threads.Take(pageSize).ToList(); + + var threadDtos = trimmed.Select(thread => new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + AnswerContent = thread.AnswerContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId ?? 0, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, + AskedId = thread.AskedId, + AskedName = thread.Asked?.Name, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0 + }).ToList(); + + var response = new PagedResponseDto + { + Items = threadDtos, + PageNumber = page, + PageSize = pageSize, + HasMore = hasMore + }; + + return await ServiceResult>.Success(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving feed"); + return await ServiceResult>.Failure(new List() { e.Message }); + } + } + + public async Task> SaveThread(int threadId, int userId) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // Check if thread exists + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == threadId); + if (thread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + // Check if thread is already saved + var existingSave = await _unitOfWork.SavedThreads.FindAsync( + st => st.SavedThreadId == threadId && st.UserId == userId + ); + + if (existingSave != null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread already saved" }); + } + + // Create saved thread + var savedThread = new SavedThreads + { + SavedThreadId = threadId, + UserId = userId, + CreatedAt = DateTime.UtcNow + }; + + // Add saved thread + await _unitOfWork.SavedThreads.AddAsync(savedThread); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + return await ServiceResult.Success(true); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error saving thread"); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task> UnsaveThread(int threadId, int userId) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // Find saved thread + var savedThread = await _unitOfWork.SavedThreads.FindAsync( + st => st.SavedThreadId == threadId && st.UserId == userId + ); + + if (savedThread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not saved" }); + } + + // Remove saved thread + await _unitOfWork.SavedThreads.RemoveAsync(savedThread); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + return await ServiceResult.Success(true); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error unsaving thread"); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetSavedThreads(int userId, int page, + int pageSize) + { + try + { + int skipCount = (page - 1) * pageSize; + + // var totalCount = await _unitOfWork.SavedThreads.CountAsync(st => st.UserId == userId); + + var savedThreads = await _unitOfWork.SavedThreads.GetPagedAsync( + skipCount, + pageSize + 1, + st => st.CreatedAt, + false, + st => st.UserId == userId, + new[] { "Thread", "Thread.Asker", "Thread.Asked", "Thread.Comments", "Thread.ThreadLikes" } + ); + + + bool hasMore = savedThreads.Count > pageSize; + + /* after getting the threads with size of pageSize + 1 + to find if there are remaining threads (nextPage) without usint TotalCound and totalPages + now we need to return back only the PageSize of threads + that what i'm doing here in the trimmed list, + */ + var trimmed = savedThreads.Take(pageSize).ToList(); + + var threadDtos = trimmed.Select(st => new ThreadResponseDto + { + Id = st.Thread.Id, + QuestionContent = st.Thread.QuestionContent, + AnswerContent = st.Thread.AnswerContent, + Status = st.Thread.Status, + IsAnonymous = st.Thread.isAnonymous, + CreatedAt = st.Thread.CreatedAt, + AskerId = st.Thread.AskerId ?? 0, + AskerName = st.Thread.isAnonymous ? "Anonymous" : st.Thread.Asker?.Name, + AskedId = st.Thread.AskedId, + AskedName = st.Thread.Asked?.Name, + LikesCount = st.Thread.ThreadLikes?.Count ?? 0, + CommentsCount = st.Thread.Comments?.Count ?? 0, + SavedAt = st.CreatedAt + }).ToList(); + + var response = new PagedResponseDto + { + Items = threadDtos, + PageNumber = page, + PageSize = pageSize, + HasMore = hasMore + }; + + return await ServiceResult>.Success(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving saved threads"); + return await ServiceResult>.Failure(new List() { e.Message }); + } + } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs index 7dcc1a9..fdbf633 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs @@ -170,7 +170,7 @@ public async Task> UpdateLastSeenAsync(int userId) await using var transaction = await _unitOfWork.BeginTransactionAsync(); try { - appUser.LastSeen = DateTime.Now; + appUser.LastSeen = DateTime.UtcNow; await _unitOfWork.Users.UpdateAsync(appUser); await _unitOfWork.SaveAsync(); await transaction.CommitAsync(); diff --git a/AskFm/AskFm.DAL/Enums/ThreadStatus.cs b/AskFm/AskFm.DAL/Enums/ThreadStatus.cs index 8bd4d98..7075457 100644 --- a/AskFm/AskFm.DAL/Enums/ThreadStatus.cs +++ b/AskFm/AskFm.DAL/Enums/ThreadStatus.cs @@ -4,5 +4,7 @@ public enum ThreadStatus PRIVATE, PUBLIC, Closed, - PRIVATEQUESTION + PRIVATEQUESTION, + Answered, + Pending } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/IRepository.cs b/AskFm/AskFm.DAL/Interfaces/IRepository.cs index 27b6962..469cd08 100644 --- a/AskFm/AskFm.DAL/Interfaces/IRepository.cs +++ b/AskFm/AskFm.DAL/Interfaces/IRepository.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; namespace AskFm.DAL.Interfaces; @@ -6,31 +7,39 @@ public interface IRepository where T : class { IQueryable GetAll(); Task> GetAllAsync(); - - + + T? GetById(int id); Task GetByIdAsync(int id); - - + + IQueryable FindAll(Expression> predicate, string[] includes = null); Task> FindAllAsync(Expression> predicate = null, string[] includes = null); - - + + T? Find(Expression> predicate, string[] includes = null); Task FindAsync(Expression> predicate, string[] includes = null); - - + + void Add(T entity); Task AddAsync(T entity); - - + + void Update(T entity); Task UpdateAsync(T entity); - - + + void Remove(T entity); Task RemoveAsync(T entity); - - + + Task CountAsync(Expression>? predicate = null); + + Task> GetPagedAsync( + int skip, + int take, + Expression> orderBy, + bool ascending = true, + Expression>? predicate = null, + string[]? includes = null); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/Repository.cs b/AskFm/AskFm.DAL/Repositories/Repository.cs index e928038..1a3a7c1 100644 --- a/AskFm/AskFm.DAL/Repositories/Repository.cs +++ b/AskFm/AskFm.DAL/Repositories/Repository.cs @@ -1,9 +1,10 @@ using System.Linq.Expressions; using AskFm.DAL.Interfaces; using Microsoft.EntityFrameworkCore; + namespace AskFm.DAL.Repositories; -public class Repository : IRepository where T : class +public class Repository : IRepository where T : class { protected readonly AppDbContext _context; private readonly DbSet _dbSet; @@ -13,21 +14,16 @@ public Repository(AppDbContext context) _context = context; _dbSet = context.Set(); } - - - + + public IQueryable GetAll() => _dbSet.AsQueryable(); public async Task> GetAllAsync() => await _dbSet.ToListAsync(); - - - - public T? GetById(int id) => _dbSet.Find(id); + + public T? GetById(int id) => _dbSet.Find(id); public async Task GetByIdAsync(int id) => await _dbSet.FindAsync(id); - - - - + + public IQueryable FindAll(Expression> predicate, string[] includes = null) { IQueryable query = _dbSet; @@ -36,6 +32,7 @@ public IQueryable FindAll(Expression> predicate, string[] inclu foreach (var include in includes) query = query.Include(include); } + return query.Where(predicate); } @@ -50,14 +47,11 @@ public async Task> FindAllAsync(Expression> predica return await query.Where(predicate).ToListAsync(); } - - - - + public T? Find(Expression> predicate, string[] includes = null) { IQueryable query = _dbSet; - + if (includes != null) { foreach (var include in includes) @@ -65,14 +59,14 @@ public async Task> FindAllAsync(Expression> predica query = query.Include(include); } } - - return query.FirstOrDefault(predicate); + + return query.FirstOrDefault(predicate); } - + public async Task FindAsync(Expression> predicate, string[] includes = null) { IQueryable query = _dbSet; - + if (includes != null) { foreach (var include in includes) @@ -80,35 +74,81 @@ public async Task> FindAllAsync(Expression> predica query = query.Include(include); } } - + return await query.FirstOrDefaultAsync(predicate); } - - - - + + public void Add(T entity) => _dbSet.Add(entity); public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity); - - - - + + public void Update(T entity) => _dbSet.Update(entity); - + public Task UpdateAsync(T entity) { _dbSet.Update(entity); return Task.CompletedTask; } - - + + public void Remove(T entity) => _dbSet.Remove(entity); - + public Task RemoveAsync(T entity) { _dbSet.Remove(entity); return Task.CompletedTask; } + public async Task CountAsync(Expression>? predicate = null) + { + IQueryable query = _dbSet; + + if (predicate != null) + { + query = query.Where(predicate); + } + + return await query.CountAsync(); + } + + public async Task> GetPagedAsync( + int skip, + int take, + Expression> orderBy, + bool ascending = true, + Expression>? predicate = null, + string[]? includes = null) + { + if (skip < 0) + throw new ArgumentOutOfRangeException(nameof(skip), "Skip must be non-negative"); + if (take <= 0) + throw new ArgumentOutOfRangeException(nameof(take), "Take must be positive"); + IQueryable query = _dbSet; + + if (predicate != null) + { + query = query.Where(predicate); + } + + if (includes != null) + { + foreach (var include in includes) + { + query = query.Include(include); + } + } + + if (ascending) + { + query = query.OrderBy(orderBy); + } + else + { + query = query.OrderByDescending(orderBy); + } + + return await query.Skip(skip).Take(take).ToListAsync(); + } } \ No newline at end of file