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 -> { .map(error -> {
String field = error.getField(); String field = error.getField();
if ("body".equals(field)) return "Post body is required and cannot exceed 2000 characters"; 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(); return error.getDefaultMessage();
}) })
.findFirst() .findFirst()
@ -52,7 +53,7 @@ public class PostController {
} }
try { try {
PostResponseDTO response = postService.createPostWithResponse(request.getBody()); PostResponseDTO response = postService.createPostWithResponse(request.getBody(), request.getImages());
return ResponseEntity.ok(ApiResponse.success(response)); return ResponseEntity.ok(ApiResponse.success(response));
} catch (RuntimeException e) { } catch (RuntimeException e) {
String message; String message;

View File

@ -2,6 +2,7 @@ package online.wesal.wesal.dto;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import java.util.List;
public class PostCreateRequestDTO { public class PostCreateRequestDTO {
@ -9,12 +10,20 @@ public class PostCreateRequestDTO {
@Size(max = 2000, message = "Post body cannot exceed 2000 characters") @Size(max = 2000, message = "Post body cannot exceed 2000 characters")
private String body; private String body;
@Size(max = 10, message = "Post cannot have more than 10 images")
private List<String> images;
public PostCreateRequestDTO() {} public PostCreateRequestDTO() {}
public PostCreateRequestDTO(String body) { public PostCreateRequestDTO(String body) {
this.body = body; this.body = body;
} }
public PostCreateRequestDTO(String body, List<String> images) {
this.body = body;
this.images = images;
}
public String getBody() { public String getBody() {
return body; return body;
} }
@ -22,4 +31,12 @@ public class PostCreateRequestDTO {
public void setBody(String body) { public void setBody(String body) {
this.body = 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.Post;
import online.wesal.wesal.entity.User; import online.wesal.wesal.entity.User;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
public class PostResponseDTO { public class PostResponseDTO {
@ -13,6 +14,7 @@ public class PostResponseDTO {
private String comments; private String comments;
private boolean liked; private boolean liked;
private LocalDateTime creationDate; private LocalDateTime creationDate;
private List<String> images;
public PostResponseDTO() {} public PostResponseDTO() {}
@ -24,6 +26,7 @@ public class PostResponseDTO {
this.comments = String.valueOf(post.getComments()); this.comments = String.valueOf(post.getComments());
this.liked = false; // Default value, will be set by service this.liked = false; // Default value, will be set by service
this.creationDate = post.getCreationDate(); this.creationDate = post.getCreationDate();
this.images = post.getImages();
} }
public PostResponseDTO(Post post, User creator, boolean liked) { public PostResponseDTO(Post post, User creator, boolean liked) {
@ -34,6 +37,7 @@ public class PostResponseDTO {
this.comments = String.valueOf(post.getComments()); this.comments = String.valueOf(post.getComments());
this.liked = liked; this.liked = liked;
this.creationDate = post.getCreationDate(); this.creationDate = post.getCreationDate();
this.images = post.getImages();
} }
public String getId() { public String getId() {
@ -91,4 +95,12 @@ public class PostResponseDTO {
public void setCreationDate(LocalDateTime creationDate) { public void setCreationDate(LocalDateTime creationDate) {
this.creationDate = 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 boolean liked;
private LocalDateTime creationDate; private LocalDateTime creationDate;
private List<LikedUserDTO> likedUsers; private List<LikedUserDTO> likedUsers;
private List<String> images;
public PostWithLikesResponseDTO() {} public PostWithLikesResponseDTO() {}
@ -27,6 +28,7 @@ public class PostWithLikesResponseDTO {
this.liked = liked; this.liked = liked;
this.creationDate = post.getCreationDate(); this.creationDate = post.getCreationDate();
this.likedUsers = likedUsers; this.likedUsers = likedUsers;
this.images = post.getImages();
} }
public String getId() { public String getId() {
@ -92,4 +94,12 @@ public class PostWithLikesResponseDTO {
public void setLikedUsers(List<LikedUserDTO> likedUsers) { public void setLikedUsers(List<LikedUserDTO> likedUsers) {
this.likedUsers = 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.persistence.*;
import jakarta.validation.constraints.NotBlank; 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.time.LocalDateTime;
import java.util.List;
import java.util.ArrayList;
@Entity @Entity
@Table(name = "posts") @Table(name = "posts")
@ -28,6 +33,9 @@ public class Post {
@Column(nullable = false) @Column(nullable = false)
private LocalDateTime creationDate = LocalDateTime.now(); private LocalDateTime creationDate = LocalDateTime.now();
@Column(columnDefinition = "TEXT")
private String images = "[]";
public Post() {} public Post() {}
public Post(Long creatorId, String body) { public Post(Long creatorId, String body) {
@ -35,6 +43,12 @@ public class Post {
this.body = body; this.body = body;
} }
public Post(Long creatorId, String body, List<String> images) {
this.creatorId = creatorId;
this.body = body;
this.setImages(images);
}
public Long getId() { public Long getId() {
return id; return id;
} }
@ -82,4 +96,33 @@ public class Post {
public void setCreationDate(LocalDateTime creationDate) { public void setCreationDate(LocalDateTime creationDate) {
this.creationDate = 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 @Autowired
private UserService userService; private UserService userService;
@Autowired
private SubscriptionNotificationService subscriptionNotificationService;
@Transactional @Transactional
public CommentResponseDTO createComment(Long postId, String body, Long replyComment) { public CommentResponseDTO createComment(Long postId, String body, Long replyComment) {
User currentUser = userService.getCurrentUser(); User currentUser = userService.getCurrentUser();
@ -70,6 +73,14 @@ public class CommentService {
post.setComments(post.getComments() + 1); post.setComments(post.getComments() + 1);
postRepository.save(post); 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); return new CommentResponseDTO(comment, currentUser, replyUser);
} }

View File

@ -34,10 +34,24 @@ public class PostService {
@Autowired @Autowired
private UserService userService; private UserService userService;
@Autowired
private SubscriptionNotificationService subscriptionNotificationService;
public Post createPost(String body) { public Post createPost(String body) {
return createPost(body, null);
}
public Post createPost(String body, List<String> images) {
User currentUser = userService.getCurrentUser(); User currentUser = userService.getCurrentUser();
Post post = new Post(currentUser.getId(), body); Post post = new Post(currentUser.getId(), body, images);
return postRepository.save(post); 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() { public List<PostResponseDTO> getAllRecentPosts() {
@ -93,9 +107,19 @@ public class PostService {
} }
public PostResponseDTO createPostWithResponse(String body) { public PostResponseDTO createPostWithResponse(String body) {
return createPostWithResponse(body, null);
}
public PostResponseDTO createPostWithResponse(String body, List<String> images) {
User currentUser = userService.getCurrentUser(); User currentUser = userService.getCurrentUser();
Post post = new Post(currentUser.getId(), body); Post post = new Post(currentUser.getId(), body, images);
post = postRepository.save(post); 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 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.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -74,4 +75,47 @@ public class SubscriptionNotificationService {
logger.error("Failed to send notifications for subscription '{}': {}", subscriptionName, e.getMessage(), e); 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() + "...";
}
} }