diff --git a/src/main/java/com/openisle/controller/AdminPostController.java b/src/main/java/com/openisle/controller/AdminPostController.java index 57e36cc50..7ab3b0ded 100644 --- a/src/main/java/com/openisle/controller/AdminPostController.java +++ b/src/main/java/com/openisle/controller/AdminPostController.java @@ -31,6 +31,11 @@ public class AdminPostController { return toDto(postService.approvePost(id)); } + @PostMapping("/{id}/reject") + public PostDto reject(@PathVariable Long id) { + return toDto(postService.rejectPost(id)); + } + private PostDto toDto(Post post) { PostDto dto = new PostDto(); dto.setId(post.getId()); diff --git a/src/main/java/com/openisle/controller/NotificationController.java b/src/main/java/com/openisle/controller/NotificationController.java new file mode 100644 index 000000000..0f9c73bf8 --- /dev/null +++ b/src/main/java/com/openisle/controller/NotificationController.java @@ -0,0 +1,62 @@ +package com.openisle.controller; + +import com.openisle.model.Notification; +import com.openisle.model.NotificationType; +import com.openisle.service.NotificationService; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** Endpoints for user notifications. */ +@RestController +@RequestMapping("/api/notifications") +@RequiredArgsConstructor +public class NotificationController { + private final NotificationService notificationService; + + @GetMapping + public List list(@RequestParam(value = "read", required = false) Boolean read, + Authentication auth) { + return notificationService.listNotifications(auth.getName(), read).stream() + .map(this::toDto) + .collect(Collectors.toList()); + } + + @PostMapping("/read") + public void markRead(@RequestBody MarkReadRequest req, Authentication auth) { + notificationService.markRead(auth.getName(), req.getIds()); + } + + private NotificationDto toDto(Notification n) { + NotificationDto dto = new NotificationDto(); + dto.setId(n.getId()); + dto.setType(n.getType()); + dto.setPostId(n.getPost() != null ? n.getPost().getId() : null); + dto.setCommentId(n.getComment() != null ? n.getComment().getId() : null); + dto.setApproved(n.getApproved()); + dto.setRead(n.isRead()); + dto.setCreatedAt(n.getCreatedAt()); + return dto; + } + + @Data + private static class MarkReadRequest { + private List ids; + } + + @Data + private static class NotificationDto { + private Long id; + private NotificationType type; + private Long postId; + private Long commentId; + private Boolean approved; + private boolean read; + private LocalDateTime createdAt; + } +} diff --git a/src/main/java/com/openisle/controller/PostController.java b/src/main/java/com/openisle/controller/PostController.java index 414ee1629..2450b7139 100644 --- a/src/main/java/com/openisle/controller/PostController.java +++ b/src/main/java/com/openisle/controller/PostController.java @@ -44,8 +44,9 @@ public class PostController { } @GetMapping("/{id}") - public ResponseEntity getPost(@PathVariable Long id) { - Post post = postService.getPost(id); + public ResponseEntity getPost(@PathVariable Long id, Authentication auth) { + String viewer = auth != null ? auth.getName() : null; + Post post = postService.viewPost(id, viewer); return ResponseEntity.ok(toDto(post)); } diff --git a/src/main/java/com/openisle/model/Notification.java b/src/main/java/com/openisle/model/Notification.java new file mode 100644 index 000000000..0529d3d0a --- /dev/null +++ b/src/main/java/com/openisle/model/Notification.java @@ -0,0 +1,52 @@ +package com.openisle.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +/** + * Entity representing a user notification. + */ +@Entity +@Getter +@Setter +@NoArgsConstructor +@Table(name = "notifications") +public class Notification { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private NotificationType type; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "comment_id") + private Comment comment; + + @Column + private Boolean approved; + + @Column(nullable = false) + private boolean read = false; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/openisle/model/NotificationType.java b/src/main/java/com/openisle/model/NotificationType.java new file mode 100644 index 000000000..d1abfbbbe --- /dev/null +++ b/src/main/java/com/openisle/model/NotificationType.java @@ -0,0 +1,15 @@ +package com.openisle.model; + +/** + * Types of user notifications. + */ +public enum NotificationType { + /** Someone viewed your post */ + POST_VIEWED, + /** Someone replied to your post or comment */ + COMMENT_REPLY, + /** Someone reacted to your post or comment */ + REACTION, + /** Your post under review was approved or rejected */ + POST_REVIEWED +} diff --git a/src/main/java/com/openisle/model/PostStatus.java b/src/main/java/com/openisle/model/PostStatus.java index e0d9f8ec1..90b921a5b 100644 --- a/src/main/java/com/openisle/model/PostStatus.java +++ b/src/main/java/com/openisle/model/PostStatus.java @@ -5,5 +5,6 @@ package com.openisle.model; */ public enum PostStatus { PUBLISHED, - PENDING + PENDING, + REJECTED } diff --git a/src/main/java/com/openisle/repository/NotificationRepository.java b/src/main/java/com/openisle/repository/NotificationRepository.java new file mode 100644 index 000000000..0ed952243 --- /dev/null +++ b/src/main/java/com/openisle/repository/NotificationRepository.java @@ -0,0 +1,13 @@ +package com.openisle.repository; + +import com.openisle.model.Notification; +import com.openisle.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +/** Repository for Notification entities. */ +public interface NotificationRepository extends JpaRepository { + List findByUserOrderByCreatedAtDesc(User user); + List findByUserAndReadOrderByCreatedAtDesc(User user, boolean read); +} diff --git a/src/main/java/com/openisle/service/CommentService.java b/src/main/java/com/openisle/service/CommentService.java index e1906bc7c..10412b01f 100644 --- a/src/main/java/com/openisle/service/CommentService.java +++ b/src/main/java/com/openisle/service/CommentService.java @@ -3,9 +3,11 @@ package com.openisle.service; import com.openisle.model.Comment; import com.openisle.model.Post; import com.openisle.model.User; +import com.openisle.model.NotificationType; import com.openisle.repository.CommentRepository; import com.openisle.repository.PostRepository; import com.openisle.repository.UserRepository; +import com.openisle.service.NotificationService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -19,6 +21,7 @@ public class CommentService { private final CommentRepository commentRepository; private final PostRepository postRepository; private final UserRepository userRepository; + private final NotificationService notificationService; public Comment addComment(String username, Long postId, String content) { User author = userRepository.findByUsername(username) @@ -29,7 +32,11 @@ public class CommentService { comment.setAuthor(author); comment.setPost(post); comment.setContent(content); - return commentRepository.save(comment); + comment = commentRepository.save(comment); + if (!author.getId().equals(post.getAuthor().getId())) { + notificationService.createNotification(post.getAuthor(), NotificationType.COMMENT_REPLY, post, comment, null); + } + return comment; } public Comment addReply(String username, Long parentId, String content) { @@ -42,7 +49,11 @@ public class CommentService { comment.setPost(parent.getPost()); comment.setParent(parent); comment.setContent(content); - return commentRepository.save(comment); + comment = commentRepository.save(comment); + if (!author.getId().equals(parent.getAuthor().getId())) { + notificationService.createNotification(parent.getAuthor(), NotificationType.COMMENT_REPLY, parent.getPost(), comment, null); + } + return comment; } public List getCommentsForPost(Long postId) { diff --git a/src/main/java/com/openisle/service/NotificationService.java b/src/main/java/com/openisle/service/NotificationService.java new file mode 100644 index 000000000..c2b7e9551 --- /dev/null +++ b/src/main/java/com/openisle/service/NotificationService.java @@ -0,0 +1,48 @@ +package com.openisle.service; + +import com.openisle.model.*; +import com.openisle.repository.NotificationRepository; +import com.openisle.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** Service for creating and retrieving notifications. */ +@Service +@RequiredArgsConstructor +public class NotificationService { + private final NotificationRepository notificationRepository; + private final UserRepository userRepository; + + public Notification createNotification(User user, NotificationType type, Post post, Comment comment, Boolean approved) { + Notification n = new Notification(); + n.setUser(user); + n.setType(type); + n.setPost(post); + n.setComment(comment); + n.setApproved(approved); + return notificationRepository.save(n); + } + + public List listNotifications(String username, Boolean read) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + if (read == null) { + return notificationRepository.findByUserOrderByCreatedAtDesc(user); + } + return notificationRepository.findByUserAndReadOrderByCreatedAtDesc(user, read); + } + + public void markRead(String username, List ids) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + List notifs = notificationRepository.findAllById(ids); + for (Notification n : notifs) { + if (n.getUser().getId().equals(user.getId())) { + n.setRead(true); + } + } + notificationRepository.saveAll(notifs); + } +} diff --git a/src/main/java/com/openisle/service/PostService.java b/src/main/java/com/openisle/service/PostService.java index 23bfca217..4edd887ec 100644 --- a/src/main/java/com/openisle/service/PostService.java +++ b/src/main/java/com/openisle/service/PostService.java @@ -5,6 +5,7 @@ import com.openisle.model.PostStatus; import com.openisle.model.PublishMode; import com.openisle.model.User; import com.openisle.model.Category; +import com.openisle.model.NotificationType; import com.openisle.repository.PostRepository; import com.openisle.repository.UserRepository; import com.openisle.repository.CategoryRepository; @@ -23,17 +24,20 @@ public class PostService { private final CategoryRepository categoryRepository; private final TagRepository tagRepository; private final PublishMode publishMode; + private final NotificationService notificationService; @org.springframework.beans.factory.annotation.Autowired public PostService(PostRepository postRepository, UserRepository userRepository, CategoryRepository categoryRepository, TagRepository tagRepository, + NotificationService notificationService, @Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) { this.postRepository = postRepository; this.userRepository = userRepository; this.categoryRepository = categoryRepository; this.tagRepository = tagRepository; + this.notificationService = notificationService; this.publishMode = publishMode; } @@ -63,14 +67,18 @@ public class PostService { return postRepository.save(post); } - public Post getPost(Long id) { + public Post viewPost(Long id, String viewer) { Post post = postRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("Post not found")); if (post.getStatus() != PostStatus.PUBLISHED) { throw new IllegalArgumentException("Post not found"); } post.setViews(post.getViews() + 1); - return postRepository.save(post); + post = postRepository.save(post); + if (viewer != null && !viewer.equals(post.getAuthor().getUsername())) { + notificationService.createNotification(post.getAuthor(), NotificationType.POST_VIEWED, post, null, null); + } + return post; } public List listPosts() { @@ -137,6 +145,17 @@ public class PostService { Post post = postRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("Post not found")); post.setStatus(PostStatus.PUBLISHED); - return postRepository.save(post); + post = postRepository.save(post); + notificationService.createNotification(post.getAuthor(), NotificationType.POST_REVIEWED, post, null, true); + return post; + } + + public Post rejectPost(Long id) { + Post post = postRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Post not found")); + post.setStatus(PostStatus.REJECTED); + post = postRepository.save(post); + notificationService.createNotification(post.getAuthor(), NotificationType.POST_REVIEWED, post, null, false); + return post; } } diff --git a/src/main/java/com/openisle/service/ReactionService.java b/src/main/java/com/openisle/service/ReactionService.java index 1de9fbedb..d58dd5b90 100644 --- a/src/main/java/com/openisle/service/ReactionService.java +++ b/src/main/java/com/openisle/service/ReactionService.java @@ -5,10 +5,12 @@ import com.openisle.model.Post; import com.openisle.model.Reaction; import com.openisle.model.ReactionType; import com.openisle.model.User; +import com.openisle.model.NotificationType; import com.openisle.repository.CommentRepository; import com.openisle.repository.PostRepository; import com.openisle.repository.ReactionRepository; import com.openisle.repository.UserRepository; +import com.openisle.service.NotificationService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -19,6 +21,7 @@ public class ReactionService { private final UserRepository userRepository; private final PostRepository postRepository; private final CommentRepository commentRepository; + private final NotificationService notificationService; public Reaction reactToPost(String username, Long postId, ReactionType type) { User user = userRepository.findByUsername(username) @@ -30,7 +33,11 @@ public class ReactionService { reaction.setUser(user); reaction.setPost(post); reaction.setType(type); - return reactionRepository.save(reaction); + reaction = reactionRepository.save(reaction); + if (!user.getId().equals(post.getAuthor().getId())) { + notificationService.createNotification(post.getAuthor(), NotificationType.REACTION, post, null, null); + } + return reaction; } public Reaction reactToComment(String username, Long commentId, ReactionType type) { @@ -44,7 +51,11 @@ public class ReactionService { reaction.setComment(comment); reaction.setPost(null); reaction.setType(type); - return reactionRepository.save(reaction); + reaction = reactionRepository.save(reaction); + if (!user.getId().equals(comment.getAuthor().getId())) { + notificationService.createNotification(comment.getAuthor(), NotificationType.REACTION, null, comment, null); + } + return reaction; } public java.util.List getReactionsForPost(Long postId) {