feat: allow images in posts

This commit is contained in:
sBubshait 2025-08-05 23:50:49 +03:00
parent 38214bd1e9
commit 55c2d231c7
8 changed files with 166 additions and 4 deletions

View File

@ -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;

View File

@ -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<String> images;
public PostCreateRequestDTO() {}
public PostCreateRequestDTO(String body) {
this.body = body;
}
public PostCreateRequestDTO(String body, List<String> 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<String> getImages() {
return images;
}
public void setImages(List<String> images) {
this.images = images;
}
}

View File

@ -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<String> 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<String> getImages() {
return images;
}
public void setImages(List<String> images) {
this.images = images;
}
}

View File

@ -15,6 +15,7 @@ public class PostWithLikesResponseDTO {
private boolean liked;
private LocalDateTime creationDate;
private List<LikedUserDTO> likedUsers;
private List<String> 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<LikedUserDTO> likedUsers) {
this.likedUsers = likedUsers;
}
public List<String> getImages() {
return images;
}
public void setImages(List<String> images) {
this.images = images;
}
}

View File

@ -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<String> 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<String> getImages() {
if (images == null || images.isEmpty()) {
return new ArrayList<>();
}
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(images, new TypeReference<List<String>>() {});
} catch (Exception e) {
return new ArrayList<>();
}
}
public void setImages(List<String> images) {
if (images == null || images.isEmpty()) {
this.images = "[]";
return;
}
// Limit to 10 images
List<String> limitedImages = images.size() > 10 ? images.subList(0, 10) : images;
try {
ObjectMapper mapper = new ObjectMapper();
this.images = mapper.writeValueAsString(limitedImages);
} catch (Exception e) {
this.images = "[]";
}
}
}

View File

@ -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);
}

View File

@ -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<String> 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<PostResponseDTO> getAllRecentPosts() {
@ -93,9 +107,19 @@ public class PostService {
}
public PostResponseDTO createPostWithResponse(String body) {
return createPostWithResponse(body, null);
}
public PostResponseDTO createPostWithResponse(String body, List<String> 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
}

View File

@ -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() + "...";
}
}