From 55c2d231c713377643d08cec1864dc8e8cbd6ed4 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Tue, 5 Aug 2025 23:50:49 +0300 Subject: [PATCH] feat: allow images in posts --- .../wesal/controller/PostController.java | 3 +- .../wesal/wesal/dto/PostCreateRequestDTO.java | 17 +++++++ .../wesal/wesal/dto/PostResponseDTO.java | 12 +++++ .../wesal/dto/PostWithLikesResponseDTO.java | 10 +++++ .../java/online/wesal/wesal/entity/Post.java | 43 ++++++++++++++++++ .../wesal/wesal/service/CommentService.java | 11 +++++ .../wesal/wesal/service/PostService.java | 30 +++++++++++-- .../SubscriptionNotificationService.java | 44 +++++++++++++++++++ 8 files changed, 166 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/online/wesal/wesal/controller/PostController.java b/backend/src/main/java/online/wesal/wesal/controller/PostController.java index fddf4e5..14f686a 100644 --- a/backend/src/main/java/online/wesal/wesal/controller/PostController.java +++ b/backend/src/main/java/online/wesal/wesal/controller/PostController.java @@ -44,6 +44,7 @@ public class PostController { .map(error -> { String field = error.getField(); if ("body".equals(field)) return "Post body is required and cannot exceed 2000 characters"; + if ("images".equals(field)) return "Post cannot have more than 10 images"; return error.getDefaultMessage(); }) .findFirst() @@ -52,7 +53,7 @@ public class PostController { } try { - PostResponseDTO response = postService.createPostWithResponse(request.getBody()); + PostResponseDTO response = postService.createPostWithResponse(request.getBody(), request.getImages()); return ResponseEntity.ok(ApiResponse.success(response)); } catch (RuntimeException e) { String message; diff --git a/backend/src/main/java/online/wesal/wesal/dto/PostCreateRequestDTO.java b/backend/src/main/java/online/wesal/wesal/dto/PostCreateRequestDTO.java index cc1fa9c..cdf752d 100644 --- a/backend/src/main/java/online/wesal/wesal/dto/PostCreateRequestDTO.java +++ b/backend/src/main/java/online/wesal/wesal/dto/PostCreateRequestDTO.java @@ -2,6 +2,7 @@ package online.wesal.wesal.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +import java.util.List; public class PostCreateRequestDTO { @@ -9,12 +10,20 @@ public class PostCreateRequestDTO { @Size(max = 2000, message = "Post body cannot exceed 2000 characters") private String body; + @Size(max = 10, message = "Post cannot have more than 10 images") + private List images; + public PostCreateRequestDTO() {} public PostCreateRequestDTO(String body) { this.body = body; } + public PostCreateRequestDTO(String body, List images) { + this.body = body; + this.images = images; + } + public String getBody() { return body; } @@ -22,4 +31,12 @@ public class PostCreateRequestDTO { public void setBody(String body) { this.body = body; } + + public List getImages() { + return images; + } + + public void setImages(List images) { + this.images = images; + } } \ No newline at end of file diff --git a/backend/src/main/java/online/wesal/wesal/dto/PostResponseDTO.java b/backend/src/main/java/online/wesal/wesal/dto/PostResponseDTO.java index 267bbfd..ccd1990 100644 --- a/backend/src/main/java/online/wesal/wesal/dto/PostResponseDTO.java +++ b/backend/src/main/java/online/wesal/wesal/dto/PostResponseDTO.java @@ -3,6 +3,7 @@ package online.wesal.wesal.dto; import online.wesal.wesal.entity.Post; import online.wesal.wesal.entity.User; import java.time.LocalDateTime; +import java.util.List; public class PostResponseDTO { @@ -13,6 +14,7 @@ public class PostResponseDTO { private String comments; private boolean liked; private LocalDateTime creationDate; + private List images; public PostResponseDTO() {} @@ -24,6 +26,7 @@ public class PostResponseDTO { this.comments = String.valueOf(post.getComments()); this.liked = false; // Default value, will be set by service this.creationDate = post.getCreationDate(); + this.images = post.getImages(); } public PostResponseDTO(Post post, User creator, boolean liked) { @@ -34,6 +37,7 @@ public class PostResponseDTO { this.comments = String.valueOf(post.getComments()); this.liked = liked; this.creationDate = post.getCreationDate(); + this.images = post.getImages(); } public String getId() { @@ -91,4 +95,12 @@ public class PostResponseDTO { public void setCreationDate(LocalDateTime creationDate) { this.creationDate = creationDate; } + + public List getImages() { + return images; + } + + public void setImages(List images) { + this.images = images; + } } \ No newline at end of file diff --git a/backend/src/main/java/online/wesal/wesal/dto/PostWithLikesResponseDTO.java b/backend/src/main/java/online/wesal/wesal/dto/PostWithLikesResponseDTO.java index bf36b86..041b1ae 100644 --- a/backend/src/main/java/online/wesal/wesal/dto/PostWithLikesResponseDTO.java +++ b/backend/src/main/java/online/wesal/wesal/dto/PostWithLikesResponseDTO.java @@ -15,6 +15,7 @@ public class PostWithLikesResponseDTO { private boolean liked; private LocalDateTime creationDate; private List likedUsers; + private List images; public PostWithLikesResponseDTO() {} @@ -27,6 +28,7 @@ public class PostWithLikesResponseDTO { this.liked = liked; this.creationDate = post.getCreationDate(); this.likedUsers = likedUsers; + this.images = post.getImages(); } public String getId() { @@ -92,4 +94,12 @@ public class PostWithLikesResponseDTO { public void setLikedUsers(List likedUsers) { this.likedUsers = likedUsers; } + + public List getImages() { + return images; + } + + public void setImages(List images) { + this.images = images; + } } \ No newline at end of file diff --git a/backend/src/main/java/online/wesal/wesal/entity/Post.java b/backend/src/main/java/online/wesal/wesal/entity/Post.java index c6e7335..cb819dd 100644 --- a/backend/src/main/java/online/wesal/wesal/entity/Post.java +++ b/backend/src/main/java/online/wesal/wesal/entity/Post.java @@ -2,7 +2,12 @@ package online.wesal.wesal.entity; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import java.time.LocalDateTime; +import java.util.List; +import java.util.ArrayList; @Entity @Table(name = "posts") @@ -28,6 +33,9 @@ public class Post { @Column(nullable = false) private LocalDateTime creationDate = LocalDateTime.now(); + @Column(columnDefinition = "TEXT") + private String images = "[]"; + public Post() {} public Post(Long creatorId, String body) { @@ -35,6 +43,12 @@ public class Post { this.body = body; } + public Post(Long creatorId, String body, List images) { + this.creatorId = creatorId; + this.body = body; + this.setImages(images); + } + public Long getId() { return id; } @@ -82,4 +96,33 @@ public class Post { public void setCreationDate(LocalDateTime creationDate) { this.creationDate = creationDate; } + + public List getImages() { + if (images == null || images.isEmpty()) { + return new ArrayList<>(); + } + try { + ObjectMapper mapper = new ObjectMapper(); + return mapper.readValue(images, new TypeReference>() {}); + } catch (Exception e) { + return new ArrayList<>(); + } + } + + public void setImages(List images) { + if (images == null || images.isEmpty()) { + this.images = "[]"; + return; + } + + // Limit to 10 images + List limitedImages = images.size() > 10 ? images.subList(0, 10) : images; + + try { + ObjectMapper mapper = new ObjectMapper(); + this.images = mapper.writeValueAsString(limitedImages); + } catch (Exception e) { + this.images = "[]"; + } + } } \ No newline at end of file diff --git a/backend/src/main/java/online/wesal/wesal/service/CommentService.java b/backend/src/main/java/online/wesal/wesal/service/CommentService.java index da084f6..eea86db 100644 --- a/backend/src/main/java/online/wesal/wesal/service/CommentService.java +++ b/backend/src/main/java/online/wesal/wesal/service/CommentService.java @@ -31,6 +31,9 @@ public class CommentService { @Autowired private UserService userService; + @Autowired + private SubscriptionNotificationService subscriptionNotificationService; + @Transactional public CommentResponseDTO createComment(Long postId, String body, Long replyComment) { User currentUser = userService.getCurrentUser(); @@ -70,6 +73,14 @@ public class CommentService { post.setComments(post.getComments() + 1); postRepository.save(post); + // Send notification to post creator if they're subscribed to "postcomments" and it's not their own comment + User postCreator = userRepository.findById(post.getCreatorId()).orElse(null); + if (postCreator != null && !postCreator.getId().equals(currentUser.getId())) { + String title = String.format("%s replied to your post", currentUser.getDisplayName()); + String excerpt = subscriptionNotificationService.truncateText(body, 60); + subscriptionNotificationService.sendNotificationToUser(postCreator, "postcomments", title, excerpt); + } + return new CommentResponseDTO(comment, currentUser, replyUser); } diff --git a/backend/src/main/java/online/wesal/wesal/service/PostService.java b/backend/src/main/java/online/wesal/wesal/service/PostService.java index 2916e53..7f7514d 100644 --- a/backend/src/main/java/online/wesal/wesal/service/PostService.java +++ b/backend/src/main/java/online/wesal/wesal/service/PostService.java @@ -34,10 +34,24 @@ public class PostService { @Autowired private UserService userService; + @Autowired + private SubscriptionNotificationService subscriptionNotificationService; + public Post createPost(String body) { + return createPost(body, null); + } + + public Post createPost(String body, List images) { User currentUser = userService.getCurrentUser(); - Post post = new Post(currentUser.getId(), body); - return postRepository.save(post); + Post post = new Post(currentUser.getId(), body, images); + Post savedPost = postRepository.save(post); + + // Send notification to users subscribed to "newposts" + String title = String.format("%s just posted", currentUser.getDisplayName()); + String excerpt = subscriptionNotificationService.truncateText(body, 80); + subscriptionNotificationService.sendNotificationToSubscription("newposts", title, excerpt); + + return savedPost; } public List getAllRecentPosts() { @@ -93,9 +107,19 @@ public class PostService { } public PostResponseDTO createPostWithResponse(String body) { + return createPostWithResponse(body, null); + } + + public PostResponseDTO createPostWithResponse(String body, List images) { User currentUser = userService.getCurrentUser(); - Post post = new Post(currentUser.getId(), body); + Post post = new Post(currentUser.getId(), body, images); post = postRepository.save(post); + + // Send notification to users subscribed to "newposts" + String title = String.format("%s just posted", currentUser.getDisplayName()); + String excerpt = subscriptionNotificationService.truncateText(body, 80); + subscriptionNotificationService.sendNotificationToSubscription("newposts", title, excerpt); + return new PostResponseDTO(post, currentUser, false); // New post is not liked by default } diff --git a/backend/src/main/java/online/wesal/wesal/service/SubscriptionNotificationService.java b/backend/src/main/java/online/wesal/wesal/service/SubscriptionNotificationService.java index 5ebcf4f..6102776 100644 --- a/backend/src/main/java/online/wesal/wesal/service/SubscriptionNotificationService.java +++ b/backend/src/main/java/online/wesal/wesal/service/SubscriptionNotificationService.java @@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -74,4 +75,47 @@ public class SubscriptionNotificationService { logger.error("Failed to send notifications for subscription '{}': {}", subscriptionName, e.getMessage(), e); } } + + @Async + public void sendNotificationToUser(User user, String subscriptionName, String title, String body) { + if (user == null || user.getFcmToken() == null || user.getFcmToken().trim().isEmpty()) { + logger.warn("User has no valid FCM token, skipping notification"); + return; + } + + // Check if user is subscribed + if (!user.getSubscriptions().contains(subscriptionName)) { + logger.debug("User {} is not subscribed to '{}', skipping notification", user.getDisplayName(), subscriptionName); + return; + } + + try { + MulticastMessage message = MulticastMessage.builder() + .setNotification(Notification.builder() + .setTitle(title) + .setBody(body) + .build()) + .addAllTokens(Collections.singletonList(user.getFcmToken())) + .build(); + + BatchResponse response = FirebaseMessaging.getInstance().sendEachForMulticast(message); + + if (response.getSuccessCount() > 0) { + logger.info("Successfully sent notification to {}", user.getDisplayName()); + } else { + logger.error("Failed to send notification to {}: {}", + user.getDisplayName(), response.getResponses().get(0).getException().getMessage()); + } + + } catch (Exception e) { + logger.error("Failed to send notification to {}: {}", user.getDisplayName(), e.getMessage(), e); + } + } + + public String truncateText(String text, int maxLength) { + if (text == null || text.length() <= maxLength) { + return text; + } + return text.substring(0, maxLength).trim() + "..."; + } } \ No newline at end of file