diff --git a/build.gradle b/build.gradle index 5a234fd..a793497 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,8 @@ repositories { } dependencies { + implementation 'org.springframework:spring-websocket:5.3.22' + implementation 'org.springframework:spring-messaging:5.3.22' implementation 'org.modelmapper:modelmapper:3.1.0' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'com.google.guava:guava:31.1-jre' diff --git a/src/main/java/site/easystartup/web/chat/a b/src/main/java/site/easystartup/web/chat/a deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/site/easystartup/web/chat/config/ChatController.java b/src/main/java/site/easystartup/web/chat/config/ChatController.java deleted file mode 100644 index 79ee23d..0000000 --- a/src/main/java/site/easystartup/web/chat/config/ChatController.java +++ /dev/null @@ -1,28 +0,0 @@ -package site.easystartup.web.chat.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.messaging.handler.annotation.SendTo; -import org.springframework.messaging.simp.SimpMessageHeaderAccessor; -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.stereotype.Controller; -import site.easystartup.web.chat.domain.model.ChatMessage; -import site.easystartup.web.chat.domain.model.ChatNotification; - -@Controller -public class ChatController { - @MessageMapping("/chat.sendMessage") - @SendTo("/topic/public") - public ChatMessage sendMessage(@Payload ChatMessage chatMessage) { - return chatMessage; - } - @MessageMapping("/chat.addUser") - @SendTo("/topic/public") - public ChatMessage addUser(@Payload ChatMessage chatMessage, - SimpMessageHeaderAccessor headerAccessor) { - // Add username in web socket session - headerAccessor.getSessionAttributes().put("username", chatMessage.getSender()); - return chatMessage; - } -} \ No newline at end of file diff --git a/src/main/java/site/easystartup/web/chat/config/WebSocketConfig.java b/src/main/java/site/easystartup/web/chat/config/WebSocketConfig.java index 34af5de..ea82539 100644 --- a/src/main/java/site/easystartup/web/chat/config/WebSocketConfig.java +++ b/src/main/java/site/easystartup/web/chat/config/WebSocketConfig.java @@ -1,22 +1,46 @@ package site.easystartup.web.chat.config; +import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.converter.DefaultContentTypeResolver; +import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.util.MimeTypeUtils; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import java.util.List; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.enableSimpleBroker( "/user"); + config.setApplicationDestinationPrefixes("/app"); + config.setUserDestinationPrefix("/user"); + } + @Override public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/ws").withSockJS(); + registry + .addEndpoint("/ws") + .setAllowedOrigins("*") + .withSockJS(); } + @Override - public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.setApplicationDestinationPrefixes("/app"); - registry.enableSimpleBroker("/topic"); + public boolean configureMessageConverters(List messageConverters) { + DefaultContentTypeResolver resolver = new DefaultContentTypeResolver(); + resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON); + MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); + converter.setObjectMapper(new ObjectMapper()); + converter.setContentTypeResolver(resolver); + messageConverters.add(converter); + return false; } -} \ No newline at end of file +} + diff --git a/src/main/java/site/easystartup/web/chat/controller/ChatController.java b/src/main/java/site/easystartup/web/chat/controller/ChatController.java new file mode 100644 index 0000000..ff710ba --- /dev/null +++ b/src/main/java/site/easystartup/web/chat/controller/ChatController.java @@ -0,0 +1,38 @@ +package site.easystartup.web.chat.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import site.easystartup.web.chat.domain.model.ChatMessage; +import site.easystartup.web.chat.domain.model.ChatNotification; +import site.easystartup.web.chat.domain.request.ChatMessageRequest; +import site.easystartup.web.chat.repo.ChatMessageRepo; +import site.easystartup.web.chat.service.ChatService; + +@Controller +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class ChatController { + private final SimpMessagingTemplate messagingTemplate; + private final ChatService chatService; + + @MessageMapping("/chat") + public void processMessage(@Payload ChatMessageRequest chatMessageRequest) { + ChatMessage chatMessage = chatService.getByRequest(chatMessageRequest); + var room = chatService + .getChatRoom(chatMessage.getRecipientName(), chatMessage.getRecipientId()); + chatMessage.setChatId(room.getChatId()); + + ChatMessage saved = chatService.save(chatMessage); + + messagingTemplate.convertAndSendToUser( + chatMessage.getRecipientId(),"/queue/messages", + new ChatNotification( + saved.getId(), + saved.getSenderId(), + saved.getSenderName())); + } +} \ No newline at end of file diff --git a/src/main/java/site/easystartup/web/chat/controller/ChatRestController.java b/src/main/java/site/easystartup/web/chat/controller/ChatRestController.java new file mode 100644 index 0000000..0a581e5 --- /dev/null +++ b/src/main/java/site/easystartup/web/chat/controller/ChatRestController.java @@ -0,0 +1,52 @@ +package site.easystartup.web.chat.controller; + +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.modelmapper.ModelMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import site.easystartup.web.chat.dto.ChatMessageDto; +import site.easystartup.web.chat.dto.ChatRoomDto; +import site.easystartup.web.chat.service.ChatService; +import site.easystartup.web.exception.NoPermissionException; +import site.easystartup.web.service.UserService; + +import java.security.Principal; +import java.util.List; + +@RestController +@RequestMapping("/chat") +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class ChatRestController { + private final ChatService chatService; + + private final ModelMapper modelMapper; + + private final UserService userService; + + @GetMapping("/{chat_id}") + public ResponseEntity> chatId(@PathVariable String chat_id, + Principal principal) { + var chatMessages = chatService.getByChatId(chat_id, principal) + .stream().map(message -> modelMapper.map(message, ChatMessageDto.class)).toList(); + return ResponseEntity.ok().body(chatMessages); + } + + @GetMapping("/see") + public ResponseEntity> chat(@RequestParam String sender, + @RequestParam String recipient, + Principal principal) { + var chatMessages = chatService.getChatMessages(sender, recipient, principal) + .stream().map(message -> modelMapper.map(message, ChatMessageDto.class)).toList(); + return ResponseEntity.ok().body(chatMessages); + } + + @GetMapping("/user/{username}") + public ResponseEntity> user(@PathVariable String username, + Principal principal) { + var chatMessages = chatService.getChats(username, principal) + .stream().map(message -> modelMapper.map(message, ChatRoomDto.class)).toList(); + return ResponseEntity.ok().body(chatMessages); + } +} diff --git a/src/main/java/site/easystartup/web/chat/domain/a b/src/main/java/site/easystartup/web/chat/domain/a deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/site/easystartup/web/chat/domain/exception/ChatRoomNotFoundException.java b/src/main/java/site/easystartup/web/chat/domain/exception/ChatRoomNotFoundException.java new file mode 100644 index 0000000..20de663 --- /dev/null +++ b/src/main/java/site/easystartup/web/chat/domain/exception/ChatRoomNotFoundException.java @@ -0,0 +1,14 @@ +package site.easystartup.web.chat.domain.exception; + +import java.util.function.Supplier; + +public class ChatRoomNotFoundException extends RuntimeException implements Supplier { + public ChatRoomNotFoundException(String message) { + super(message); + } + + @Override + public ChatRoomNotFoundException get() { + return this; + } +} diff --git a/src/main/java/site/easystartup/web/chat/domain/model/ChatMessage.java b/src/main/java/site/easystartup/web/chat/domain/model/ChatMessage.java index a307a78..c19fdd8 100644 --- a/src/main/java/site/easystartup/web/chat/domain/model/ChatMessage.java +++ b/src/main/java/site/easystartup/web/chat/domain/model/ChatMessage.java @@ -1,15 +1,51 @@ package site.easystartup.web.chat.domain.model; -import lombok.Data; +import lombok.*; +import org.hibernate.Hibernate; +import site.easystartup.web.forum.domain.model.DiscussionMessage; -@Data -public class ChatMessage { - private MessageType type; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import java.util.Date; +import java.util.Objects; + +@Getter +@Setter +@ToString +@RequiredArgsConstructor +@Entity +public class ChatMessage implements Comparable { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long id; + private String chatId; + private String senderId; + private String recipientId; + private String senderName; + private String recipientName; private String content; - private String sender; - public enum MessageType { - CHAT, - JOIN, - LEAVE + private Date timestamp; + private MessageStatus status; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; + ChatMessage that = (ChatMessage) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public int compareTo(ChatMessage o) { + if (getTimestamp() == null || o.getTimestamp() == null) + return 0; + return getTimestamp().compareTo(o.getTimestamp()); } } \ No newline at end of file diff --git a/src/main/java/site/easystartup/web/chat/domain/model/ChatNotification.java b/src/main/java/site/easystartup/web/chat/domain/model/ChatNotification.java index fa60b73..dd7d904 100644 --- a/src/main/java/site/easystartup/web/chat/domain/model/ChatNotification.java +++ b/src/main/java/site/easystartup/web/chat/domain/model/ChatNotification.java @@ -1,11 +1,10 @@ package site.easystartup.web.chat.domain.model; import lombok.AllArgsConstructor; -import lombok.Data; -@Data @AllArgsConstructor public class ChatNotification { - private String id; + private long id; + private String senderId; private String senderName; } \ No newline at end of file diff --git a/src/main/java/site/easystartup/web/chat/domain/model/ChatRoom.java b/src/main/java/site/easystartup/web/chat/domain/model/ChatRoom.java index 70e96b0..18b9f5e 100644 --- a/src/main/java/site/easystartup/web/chat/domain/model/ChatRoom.java +++ b/src/main/java/site/easystartup/web/chat/domain/model/ChatRoom.java @@ -1,11 +1,46 @@ package site.easystartup.web.chat.domain.model; -import lombok.Data; +import lombok.*; +import org.hibernate.Hibernate; -@Data -public class ChatRoom { - private String id; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import java.util.Date; +import java.util.Objects; + +@Getter +@Setter +@ToString +@RequiredArgsConstructor +@Entity +public class ChatRoom implements Comparable { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long id; private String chatId; - private String senderId; - private String recipientId; + private String sender; + private String recipient; + private Date last_update; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; + ChatRoom chatRoom = (ChatRoom) o; + return Objects.equals(id, chatRoom.id); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public int compareTo(ChatRoom o) { + if (getLast_update() == null || o.getLast_update() == null) + return 0; + return getLast_update().compareTo(o.getLast_update()); + } } \ No newline at end of file diff --git a/src/main/java/site/easystartup/web/chat/domain/request/ChatMessageRequest.java b/src/main/java/site/easystartup/web/chat/domain/request/ChatMessageRequest.java new file mode 100644 index 0000000..928243a --- /dev/null +++ b/src/main/java/site/easystartup/web/chat/domain/request/ChatMessageRequest.java @@ -0,0 +1,27 @@ +package site.easystartup.web.chat.domain.request; + +import lombok.Data; +import site.easystartup.web.chat.domain.model.MessageStatus; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.Date; + +@Data +public class ChatMessageRequest { + private long id; + private String chatId; + @NotNull + private String senderId; + @NotNull + private String recipientId; + @NotNull + private String senderName; + @NotNull + private String recipientName; + @NotNull + @Size(min = 1, max = 255, message = "Text should be no less 1 and no more 255 signs") + private String content; + private Date timestamp; + private MessageStatus status; +} diff --git a/src/main/java/site/easystartup/web/chat/dto/ChatMessageDto.java b/src/main/java/site/easystartup/web/chat/dto/ChatMessageDto.java new file mode 100644 index 0000000..f587a6b --- /dev/null +++ b/src/main/java/site/easystartup/web/chat/dto/ChatMessageDto.java @@ -0,0 +1,28 @@ +package site.easystartup.web.chat.dto; + +import lombok.Data; +import site.easystartup.web.chat.domain.model.MessageStatus; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Date; + +@Data +public class ChatMessageDto implements Serializable { + private long id; + private String chatId; + @NotNull + private String senderId; + @NotNull + private String recipientId; + @NotNull + private String senderName; + @NotNull + private String recipientName; + @NotNull + @Size(min = 1, max = 255, message = "Text should be no less 1 and no more 255 signs") + private String content; + private Date timestamp; + private MessageStatus status; +} diff --git a/src/main/java/site/easystartup/web/chat/dto/ChatRoomDto.java b/src/main/java/site/easystartup/web/chat/dto/ChatRoomDto.java new file mode 100644 index 0000000..272cd02 --- /dev/null +++ b/src/main/java/site/easystartup/web/chat/dto/ChatRoomDto.java @@ -0,0 +1,11 @@ +package site.easystartup.web.chat.dto; + +import lombok.Data; + +@Data +public class ChatRoomDto { + private long id; + private String chatId; + private String sender; + private String recipient; +} diff --git a/src/main/java/site/easystartup/web/chat/event/WebSocketEventListener.java b/src/main/java/site/easystartup/web/chat/event/WebSocketEventListener.java deleted file mode 100644 index 4823b97..0000000 --- a/src/main/java/site/easystartup/web/chat/event/WebSocketEventListener.java +++ /dev/null @@ -1,35 +0,0 @@ -package site.easystartup.web.chat.event; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.event.EventListener; -import org.springframework.messaging.simp.SimpMessageSendingOperations; -import org.springframework.messaging.simp.stomp.StompHeaderAccessor; -import org.springframework.stereotype.Component; -import org.springframework.web.socket.messaging.SessionConnectedEvent; -import org.springframework.web.socket.messaging.SessionDisconnectEvent; -import site.easystartup.web.chat.domain.model.ChatMessage; - -@Component -public class WebSocketEventListener { - private static final Logger logger = LoggerFactory.getLogger(WebSocketEventListener.class); - @Autowired - private SimpMessageSendingOperations messagingTemplate; - @EventListener - public void handleWebSocketConnectListener(SessionConnectedEvent event) { - logger.info("Received a new web socket connection"); - } - @EventListener - public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) { - StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage()); - String username = (String) headerAccessor.getSessionAttributes().get("username"); - if(username != null) { - logger.info("User Disconnected : " + username); - ChatMessage chatMessage = new ChatMessage(); - chatMessage.setType(ChatMessage.MessageType.LEAVE); - chatMessage.setSender(username); - messagingTemplate.convertAndSend("/topic/public", chatMessage); - } - } -} \ No newline at end of file diff --git a/src/main/java/site/easystartup/web/chat/repo/ChatMessageRepo.java b/src/main/java/site/easystartup/web/chat/repo/ChatMessageRepo.java new file mode 100644 index 0000000..886c513 --- /dev/null +++ b/src/main/java/site/easystartup/web/chat/repo/ChatMessageRepo.java @@ -0,0 +1,8 @@ +package site.easystartup.web.chat.repo; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.CrudRepository; +import site.easystartup.web.chat.domain.model.ChatMessage; + +public interface ChatMessageRepo extends JpaRepository { +} diff --git a/src/main/java/site/easystartup/web/chat/repo/ChatRoomRepo.java b/src/main/java/site/easystartup/web/chat/repo/ChatRoomRepo.java new file mode 100644 index 0000000..a08c895 --- /dev/null +++ b/src/main/java/site/easystartup/web/chat/repo/ChatRoomRepo.java @@ -0,0 +1,8 @@ +package site.easystartup.web.chat.repo; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.CrudRepository; +import site.easystartup.web.chat.domain.model.ChatRoom; + +public interface ChatRoomRepo extends JpaRepository { +} diff --git a/src/main/java/site/easystartup/web/chat/service/ChatService.java b/src/main/java/site/easystartup/web/chat/service/ChatService.java new file mode 100644 index 0000000..5658003 --- /dev/null +++ b/src/main/java/site/easystartup/web/chat/service/ChatService.java @@ -0,0 +1,101 @@ +package site.easystartup.web.chat.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; +import site.easystartup.web.chat.domain.exception.ChatRoomNotFoundException; +import site.easystartup.web.chat.domain.model.ChatMessage; +import site.easystartup.web.chat.domain.model.ChatRoom; +import site.easystartup.web.chat.domain.request.ChatMessageRequest; +import site.easystartup.web.chat.dto.ChatRoomDto; +import site.easystartup.web.chat.repo.ChatMessageRepo; +import site.easystartup.web.chat.repo.ChatRoomRepo; +import site.easystartup.web.exception.NoPermissionException; +import site.easystartup.web.service.UserService; + +import java.security.Principal; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +@Component +@Scope("singleton") +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class ChatService { + private final ChatMessageRepo chatMessageRepo; + private final ChatRoomRepo chatRoomRepo; + private final UserService userService; + + public ChatRoom getChatRoom(String sender, String recipient) { + return chatRoomRepo.findAll().stream() + .filter(chatRoom -> chatRoom.getSender().equals(sender) + && chatRoom.getRecipient().equals(recipient)).findAny() + .orElse(createChatRoom(sender, recipient)); + } + + public List getChatMessages(String sender, String recipient, Principal principal) { + if(!getChatRoom(sender, recipient).getSender().equals(principal.getName()) + || !(userService.getUserByPrincipal(principal)).isAdmin()) + throw new NoPermissionException( + String.format("User %s dont have perms to see chat %s / %s", sender, recipient)); + + List result = new java.util.ArrayList<>(chatMessageRepo.findAll().stream().filter( + chatMessage -> chatMessage.getSenderName().equals(sender) && chatMessage.getSenderName().equals(recipient) + ).toList()); + result.sort(ChatMessage::compareTo); + return result; + } + + public List getChats(String sender, Principal principal) { + if(!principal.getName().equals(sender) + || !(userService.getUserByPrincipal(principal)).isAdmin()) + throw new NoPermissionException( + String.format("User %s dont have perms to see chats %s", sender)); + + List result = new java.util.ArrayList<>(chatRoomRepo.findAll().stream().filter( + chatRoom -> chatRoom.getSender().equals(sender) + ).toList()); + result.sort(ChatRoom::compareTo); + return result; + } + + public ChatRoom getChatRoom(String chat_id) { + return chatRoomRepo.findAll().stream() + .filter(chatRoom -> chatRoom.getChatId().equals(chat_id)).findAny() + .orElseThrow(new ChatRoomNotFoundException("Chat with id " + chat_id + " dont exists!")); + } + + public ChatMessage getByRequest(ChatMessageRequest chatMessageRequest) { + ChatMessage result = new ChatMessage(); + result.setContent(chatMessageRequest.getContent()); + result.setRecipientId(chatMessageRequest.getRecipientId()); + result.setRecipientName(chatMessageRequest.getRecipientName()); + result.setSenderId(chatMessageRequest.getSenderId()); + result.setSenderName(chatMessageRequest.getSenderName()); + result.setContent(chatMessageRequest.getContent()); + result.setTimestamp(chatMessageRequest.getTimestamp() != null ? chatMessageRequest.getTimestamp() : new Date()); + return result; + } + + public ChatMessage save(ChatMessage chatMessage) { + return chatMessageRepo.save(chatMessage); + } + + public List getByChatId(String chat_id, Principal principal) { + if(!getChatRoom(chat_id).getSender().equals(principal.getName()) + || !(userService.getUserByPrincipal(principal)).isAdmin()) + throw new NoPermissionException( + String.format("User %s dont have perms to see chat %s", principal.getName(), chat_id)); + + return chatMessageRepo.findAll().stream().filter(chatMessage -> chatMessage.getChatId().equals(chat_id)).toList(); + } + + public ChatRoom createChatRoom(String sender, String recipient) { + ChatRoom chatRoom = new ChatRoom(); + chatRoom.setSender(sender); + chatRoom.setRecipient(recipient); + chatRoom.setChatId(UUID.randomUUID().toString()); + return chatRoomRepo.save(chatRoom); + } +} diff --git a/src/main/java/site/easystartup/web/forum/controller/ForumController.java b/src/main/java/site/easystartup/web/forum/controller/ForumController.java index fec129f..4d17a36 100644 --- a/src/main/java/site/easystartup/web/forum/controller/ForumController.java +++ b/src/main/java/site/easystartup/web/forum/controller/ForumController.java @@ -139,7 +139,7 @@ public class ForumController { return ResponseEntity.ok().body(modelMapper.map(messageUpdated, DiscussionMessageDto.class)); } - @DeleteMapping("/forum/message/{message_id}/delete") + @DeleteMapping("/message/{message_id}/delete") public ResponseEntity messageDelete(@PathVariable long message_id, Principal principal) { DiscussionMessage message = forumService.getDiscussionMessageById(message_id); User user = userService.getUserByPrincipal(principal); diff --git a/src/main/java/site/easystartup/web/forum/domain/model/DiscussionMessage.java b/src/main/java/site/easystartup/web/forum/domain/model/DiscussionMessage.java index d04e4af..9c22fb6 100644 --- a/src/main/java/site/easystartup/web/forum/domain/model/DiscussionMessage.java +++ b/src/main/java/site/easystartup/web/forum/domain/model/DiscussionMessage.java @@ -13,7 +13,7 @@ import java.util.Objects; @ToString @RequiredArgsConstructor @Entity -public class DiscussionMessage { +public class DiscussionMessage implements Comparable{ @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; @@ -40,4 +40,12 @@ public class DiscussionMessage { public int hashCode() { return getClass().hashCode(); } + + @Override + public int compareTo(DiscussionMessage o) { + if (getDate() == null || o.getDate() == null) + return 0; + return getDate().compareTo(o.getDate()); + } + } \ No newline at end of file diff --git a/src/main/java/site/easystartup/web/forum/dto/DiscussionDto.java b/src/main/java/site/easystartup/web/forum/dto/DiscussionDto.java index 4e879b1..38fb834 100644 --- a/src/main/java/site/easystartup/web/forum/dto/DiscussionDto.java +++ b/src/main/java/site/easystartup/web/forum/dto/DiscussionDto.java @@ -5,11 +5,12 @@ import site.easystartup.web.forum.domain.model.DiscussionStatus; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; +import java.io.Serializable; import java.util.Date; import java.util.List; @Data -public class DiscussionDto { +public class DiscussionDto implements Serializable { private long id; @Size(min = 3, max = 100, message = "Title should be no less 3 and no more 100 signs") @NotNull diff --git a/src/main/java/site/easystartup/web/forum/dto/DiscussionMessageDto.java b/src/main/java/site/easystartup/web/forum/dto/DiscussionMessageDto.java index ee217ad..caf2c45 100644 --- a/src/main/java/site/easystartup/web/forum/dto/DiscussionMessageDto.java +++ b/src/main/java/site/easystartup/web/forum/dto/DiscussionMessageDto.java @@ -4,10 +4,11 @@ import lombok.Data; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; +import java.io.Serializable; import java.util.Date; @Data -public class DiscussionMessageDto { +public class DiscussionMessageDto implements Serializable { private long id; @Size(min = 3, max = 1024, message = "Text should be no less 3 and no more 1024 signs") @NotNull diff --git a/src/main/java/site/easystartup/web/forum/dto/TopicDto.java b/src/main/java/site/easystartup/web/forum/dto/TopicDto.java index d98d3a0..dd6c4cb 100644 --- a/src/main/java/site/easystartup/web/forum/dto/TopicDto.java +++ b/src/main/java/site/easystartup/web/forum/dto/TopicDto.java @@ -4,10 +4,11 @@ import lombok.Data; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; +import java.io.Serializable; import java.util.Date; @Data -public class TopicDto { +public class TopicDto implements Serializable { private long id; @Size(min = 3, max = 100, message = "Title should be no less 3 and no more 100 signs") @NotNull diff --git a/src/main/java/site/easystartup/web/forum/repo/DiscussionMessageRepo.java b/src/main/java/site/easystartup/web/forum/repo/DiscussionMessageRepo.java index f62e058..1e24b56 100644 --- a/src/main/java/site/easystartup/web/forum/repo/DiscussionMessageRepo.java +++ b/src/main/java/site/easystartup/web/forum/repo/DiscussionMessageRepo.java @@ -1,7 +1,8 @@ package site.easystartup.web.forum.repo; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.CrudRepository; import site.easystartup.web.forum.domain.model.DiscussionMessage; -public interface DiscussionMessageRepo extends CrudRepository { +public interface DiscussionMessageRepo extends JpaRepository { } diff --git a/src/main/java/site/easystartup/web/forum/repo/DiscussionRepo.java b/src/main/java/site/easystartup/web/forum/repo/DiscussionRepo.java index a036586..5025b09 100644 --- a/src/main/java/site/easystartup/web/forum/repo/DiscussionRepo.java +++ b/src/main/java/site/easystartup/web/forum/repo/DiscussionRepo.java @@ -1,7 +1,8 @@ package site.easystartup.web.forum.repo; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.CrudRepository; import site.easystartup.web.forum.domain.model.Discussion; -public interface DiscussionRepo extends CrudRepository { +public interface DiscussionRepo extends JpaRepository { } diff --git a/src/main/java/site/easystartup/web/forum/repo/TopicRepo.java b/src/main/java/site/easystartup/web/forum/repo/TopicRepo.java index 35b91ce..08c49d4 100644 --- a/src/main/java/site/easystartup/web/forum/repo/TopicRepo.java +++ b/src/main/java/site/easystartup/web/forum/repo/TopicRepo.java @@ -1,7 +1,8 @@ package site.easystartup.web.forum.repo; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.CrudRepository; import site.easystartup.web.forum.domain.model.Topic; -public interface TopicRepo extends CrudRepository { +public interface TopicRepo extends JpaRepository { } diff --git a/src/main/java/site/easystartup/web/forum/service/ForumService.java b/src/main/java/site/easystartup/web/forum/service/ForumService.java index d454804..8e020ea 100644 --- a/src/main/java/site/easystartup/web/forum/service/ForumService.java +++ b/src/main/java/site/easystartup/web/forum/service/ForumService.java @@ -45,8 +45,10 @@ public class ForumService { } public List getMessagesByDiscussionId(long discussionId) { - return Lists.newArrayList(discussionMessageRepo.findAll()).stream() - .filter(m -> m.getDiscussion_id() == discussionId).toList(); + List result = new ArrayList(Lists.newArrayList(discussionMessageRepo.findAll()).stream() + .filter(m -> m.getDiscussion_id() == discussionId).toList()); + result.sort(DiscussionMessage::compareTo); + return result; } public Discussion getDiscussionById(long discussion_id) { @@ -55,14 +57,18 @@ public class ForumService { } public List getDiscussionsByAuthor(Principal principal) { - return Lists.newArrayList(discussionRepo.findAll()).stream() - .filter(d -> d.getAuthor().equals(principal.getName())).toList(); + List result = new ArrayList<>(Lists.newArrayList(discussionRepo.findAll()).stream() + .filter(d -> d.getAuthor().equals(principal.getName())).toList()); + result.sort(Discussion::compareTo); + return result; } public List getDiscussionsByMember(Principal principal) { - return Lists.newArrayList(discussionRepo.findAll()).stream() + List result = new ArrayList<>(Lists.newArrayList(discussionRepo.findAll()).stream() .filter(d -> getMessagesByDiscussionId(d.getId()).stream() - .anyMatch(m -> m.getSender().equals(principal.getName()))).toList(); + .anyMatch(m -> m.getSender().equals(principal.getName()))).toList()); + result.sort(Discussion::compareTo); + return result; } public List getPinnedDiscussionsByTopicId(long topic_id) { @@ -72,10 +78,10 @@ public class ForumService { } public List getSortedDiscussionsByTopicId(long topic_id) { - List result = Lists.newArrayList(discussionRepo.findAll()).stream() + List result = new ArrayList<>(Lists.newArrayList(discussionRepo.findAll()).stream() .filter(d -> !d.isPinned()) - .filter(d -> d.getTopic() == topic_id).toList(); - Collections.sort(result, Discussion::compareTo); + .filter(d -> d.getTopic() == topic_id).toList()); + result.sort(Discussion::compareTo); return result; } diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css deleted file mode 100644 index 345f48a..0000000 --- a/src/main/resources/static/css/main.css +++ /dev/null @@ -1,251 +0,0 @@ -* { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -html,body { - height: 100%; - overflow: hidden; -} -body { - margin: 0; - padding: 0; - font-weight: 400; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 1rem; - line-height: 1.58; - color: #333; - background-color: #f4f4f4; - height: 100%; -} -body:before { - height: 50%; - width: 100%; - position: absolute; - top: 0; - left: 0; - background: #128ff2; - content: ""; - z-index: 0; -} -.clearfix:after { - display: block; - content: ""; - clear: both; -} -.hidden { - display: none; -} -.form-control { - width: 100%; - min-height: 38px; - font-size: 15px; - border: 1px solid #c8c8c8; -} -.form-group { - margin-bottom: 15px; -} -input { - padding-left: 10px; - outline: none; -} -h1, h2, h3, h4, h5, h6 { - margin-top: 20px; - margin-bottom: 20px; -} -h1 { - font-size: 1.7em; -} -a { - color: #128ff2; -} -button { - box-shadow: none; - border: 1px solid transparent; - font-size: 14px; - outline: none; - line-height: 100%; - white-space: nowrap; - vertical-align: middle; - padding: 0.6rem 1rem; - border-radius: 2px; - transition: all 0.2s ease-in-out; - cursor: pointer; - min-height: 38px; -} -button.default { - background-color: #e8e8e8; - color: #333; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12); -} -button.primary { - background-color: #128ff2; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12); - color: #fff; -} -button.accent { - background-color: #ff4743; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12); - color: #fff; -} -#username-page { - text-align: center; -} -.username-page-container { - background: #fff; - box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27); - border-radius: 2px; - width: 100%; - max-width: 500px; - display: inline-block; - margin-top: 42px; - vertical-align: middle; - position: relative; - padding: 35px 55px 35px; - min-height: 250px; - position: absolute; - top: 50%; - left: 0; - right: 0; - margin: 0 auto; - margin-top: -160px; -} -.username-page-container .username-submit { - margin-top: 10px; -} -#chat-page { - position: relative; - height: 100%; -} -.chat-container { - max-width: 700px; - margin-left: auto; - margin-right: auto; - background-color: #fff; - box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27); - margin-top: 30px; - height: calc(100% - 60px); - max-height: 600px; - position: relative; -} -#chat-page ul { - list-style-type: none; - background-color: #FFF; - margin: 0; - overflow: auto; - overflow-y: scroll; - padding: 0 20px 0px 20px; - height: calc(100% - 150px); -} -#chat-page #messageForm { - padding: 20px; -} -#chat-page ul li { - line-height: 1.5rem; - padding: 10px 20px; - margin: 0; - border-bottom: 1px solid #f4f4f4; -} -#chat-page ul li p { - margin: 0; -} -#chat-page .event-message { - width: 100%; - text-align: center; - clear: both; -} -#chat-page .event-message p { - color: #777; - font-size: 14px; - word-wrap: break-word; -} -#chat-page .chat-message { - padding-left: 68px; - position: relative; -} -#chat-page .chat-message i { - position: absolute; - width: 42px; - height: 42px; - overflow: hidden; - left: 10px; - display: inline-block; - vertical-align: middle; - font-size: 18px; - line-height: 42px; - color: #fff; - text-align: center; - border-radius: 50%; - font-style: normal; - text-transform: uppercase; -} -#chat-page .chat-message span { - color: #333; - font-weight: 600; -} -#chat-page .chat-message p { - color: #43464b; -} -#messageForm .input-group input { - float: left; - width: calc(100% - 85px); -} -#messageForm .input-group button { - float: left; - width: 80px; - height: 38px; - margin-left: 5px; -} -.chat-header { - text-align: center; - padding: 15px; - border-bottom: 1px solid #ececec; -} -.chat-header h2 { - margin: 0; - font-weight: 500; -} -.connecting { - padding-top: 5px; - text-align: center; - color: #777; - position: absolute; - top: 65px; - width: 100%; -} -@media screen and (max-width: 730px) { - .chat-container { - margin-left: 10px; - margin-right: 10px; - margin-top: 10px; - } -} -@media screen and (max-width: 480px) { - .chat-container { - height: calc(100% - 30px); - } - .username-page-container { - width: auto; - margin-left: 15px; - margin-right: 15px; - padding: 25px; - } - #chat-page ul { - height: calc(100% - 120px); - } - #messageForm .input-group button { - width: 65px; - } - #messageForm .input-group input { - width: calc(100% - 70px); - } - .chat-header { - padding: 10px; - } - .connecting { - top: 60px; - } - .chat-header h2 { - font-size: 1.1em; - } -} \ No newline at end of file diff --git a/src/main/resources/static/images/logo_black.png b/src/main/resources/static/images/logo_black.png deleted file mode 100644 index 01e76af..0000000 Binary files a/src/main/resources/static/images/logo_black.png and /dev/null differ diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html deleted file mode 100644 index 694e4ea..0000000 --- a/src/main/resources/static/index.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - Spring Boot WebSocket Chat Application - - - - -
-
-

Type your username

-
-
- -
-
- -
-
-
-
- - - - - - \ No newline at end of file diff --git a/src/main/resources/static/js/main.js b/src/main/resources/static/js/main.js index f86bada..be49c38 100644 --- a/src/main/resources/static/js/main.js +++ b/src/main/resources/static/js/main.js @@ -1,90 +1,30 @@ -'use strict'; -var usernamePage = document.querySelector('#username-page'); -var chatPage = document.querySelector('#chat-page'); -var usernameForm = document.querySelector('#usernameForm'); -var messageForm = document.querySelector('#messageForm'); -var messageInput = document.querySelector('#message'); -var messageArea = document.querySelector('#messageArea'); -var connectingElement = document.querySelector('.connecting'); -var stompClient = null; -var username = null; -var colors = [ - '#2196F3', '#32c787', '#00BCD4', '#ff5652', - '#ffc107', '#ff85af', '#FF9800', '#39bbb0' -]; -function connect(event) { - username = document.querySelector('#name').value.trim(); - if(username) { - usernamePage.classList.add('hidden'); - chatPage.classList.remove('hidden'); - var socket = new SockJS('/ws'); - stompClient = Stomp.over(socket); - stompClient.connect({}, onConnected, onError); +const connect = () => { + const Stomp = require("stompjs"); + var SockJS = require("sockjs-client"); + SockJS = new SockJS("http://localhost:8080/ws"); + stompClient = Stomp.over(SockJS); + stompClient.connect({}, onConnected, onError); + }; + + const onConnected = () => { + console.log("connected"); + + stompClient.subscribe( + "/user/" + currentUser.id + "/queue/messages", + onMessageReceived + ); + }; + + const sendMessage = (msg) => { + if (msg.trim() !== "") { + const message = { + senderId: currentUser.id, + recipientId: activeContact.id, + senderName: currentUser.name, + recipientName: activeContact.name, + content: msg, + }; + + stompClient.send("/app/chat", {}, JSON.stringify(message)); } - event.preventDefault(); -} -function onConnected() { - // Subscribe to the Public Topic - stompClient.subscribe('/topic/public', onMessageReceived); - // Tell your username to the server - stompClient.send("/app/chat.addUser", - {}, - JSON.stringify({sender: username, type: 'JOIN'}) - ) - connectingElement.classList.add('hidden'); -} -function onError(error) { - connectingElement.textContent = 'Could not connect to WebSocket server. Please refresh this page to try again!'; - connectingElement.style.color = 'red'; -} -function sendMessage(event) { - var messageContent = messageInput.value.trim(); - if(messageContent && stompClient) { - var chatMessage = { - sender: username, - content: messageInput.value, - type: 'CHAT' - }; - stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage)); - messageInput.value = ''; - } - event.preventDefault(); -} -function onMessageReceived(payload) { - var message = JSON.parse(payload.body); - var messageElement = document.createElement('li'); - if(message.type === 'JOIN') { - messageElement.classList.add('event-message'); - message.content = message.sender + ' joined!'; - } else if (message.type === 'LEAVE') { - messageElement.classList.add('event-message'); - message.content = message.sender + ' left!'; - } else { - messageElement.classList.add('chat-message'); - var avatarElement = document.createElement('i'); - var avatarText = document.createTextNode(message.sender[0]); - avatarElement.appendChild(avatarText); - avatarElement.style['background-color'] = getAvatarColor(message.sender); - messageElement.appendChild(avatarElement); - var usernameElement = document.createElement('span'); - var usernameText = document.createTextNode(message.sender); - usernameElement.appendChild(usernameText); - messageElement.appendChild(usernameElement); - } - var textElement = document.createElement('p'); - var messageText = document.createTextNode(message.content); - textElement.appendChild(messageText); - messageElement.appendChild(textElement); - messageArea.appendChild(messageElement); - messageArea.scrollTop = messageArea.scrollHeight; -} -function getAvatarColor(messageSender) { - var hash = 0; - for (var i = 0; i < messageSender.length; i++) { - hash = 31 * hash + messageSender.charCodeAt(i); - } - var index = Math.abs(hash % colors.length); - return colors[index]; -} -usernameForm.addEventListener('submit', connect, true) -messageForm.addEventListener('submit', sendMessage, true) \ No newline at end of file + };