diff --git a/backend/build.gradle b/backend/build.gradle index 57b428ee..91cf88ad 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -35,7 +35,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-websocket' implementation 'org.flywaydb:flyway-core' implementation 'io.jsonwebtoken:jjwt-api:0.12.3' - implementation 'org.springframework.session:spring-session-core' implementation 'se.michaelthelin.spotify:spotify-web-api-java:8.0.0' implementation 'me.paulschwarz:spring-dotenv:4.0.0' diff --git a/backend/src/main/java/org/kaiteki/backend/config/SecurityConfiguration.java b/backend/src/main/java/org/kaiteki/backend/config/SecurityConfiguration.java index 001ef1ad..2841a839 100644 --- a/backend/src/main/java/org/kaiteki/backend/config/SecurityConfiguration.java +++ b/backend/src/main/java/org/kaiteki/backend/config/SecurityConfiguration.java @@ -23,6 +23,7 @@ public class SecurityConfiguration { private static final String[] WHITE_LIST_URL = { "/ws/**", + "/api/v1/zoom/**", "/api/v1/users/current", "/api/v1/demo/anonymous", "/api/v1/auth/**", diff --git a/backend/src/main/java/org/kaiteki/backend/config/WebSocketConfiguration.java b/backend/src/main/java/org/kaiteki/backend/config/WebSocketConfiguration.java index efae5ba1..39dd0239 100644 --- a/backend/src/main/java/org/kaiteki/backend/config/WebSocketConfiguration.java +++ b/backend/src/main/java/org/kaiteki/backend/config/WebSocketConfiguration.java @@ -1,11 +1,16 @@ package org.kaiteki.backend.config; +import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Value; 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.web.socket.config.annotation.EnableWebSocketMessageBroker; -import org.springframework.web.socket.config.annotation.StompEndpointRegistry; -import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import org.springframework.util.MimeTypeUtils; +import org.springframework.web.socket.config.annotation.*; + +import java.util.List; @Configuration @@ -13,16 +18,25 @@ public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { @Value("${application.client.url}") private String clientUrl; + @Override - public void configureMessageBroker(MessageBrokerRegistry config) { - config.enableSimpleBroker("/queue"); // Destination for messages to be broadcast - config.setApplicationDestinationPrefixes("/app"); // Prefix for messages to be routed to controllers + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setApplicationDestinationPrefixes("/app"); + registry.enableSimpleBroker("/chats", "/user", "/topic"); + registry.setUserDestinationPrefix("/user"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { - registry - .addEndpoint("/ws") - .setAllowedOrigins(clientUrl); + registry.addEndpoint("/ws") + .setAllowedOrigins(clientUrl) + .setAllowedOriginPatterns("*"); + } + + @Override + public void configureWebSocketTransport(WebSocketTransportRegistration registry) { + registry.setSendTimeLimit(60 * 1000) + .setSendBufferSizeLimit(50 * 1024 * 1024) + .setMessageSizeLimit(50 * 1024 * 1024); } } \ No newline at end of file diff --git a/backend/src/main/java/org/kaiteki/backend/config/jwt/JwtAuthenticationFilter.java b/backend/src/main/java/org/kaiteki/backend/config/jwt/JwtAuthenticationFilter.java index 6bda5703..dbb5f1c3 100644 --- a/backend/src/main/java/org/kaiteki/backend/config/jwt/JwtAuthenticationFilter.java +++ b/backend/src/main/java/org/kaiteki/backend/config/jwt/JwtAuthenticationFilter.java @@ -52,15 +52,19 @@ protected void doFilterInternal( filterChain.doFilter(request, response); } catch (JwtException | SecurityException e) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid or unauthorized token: " + e.getMessage()); + if (!response.isCommitted()) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid or unauthorized token: " + e.getMessage()); + } } catch (Exception e) { - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Authentication verification failed: " + e.getMessage()); + if (!response.isCommitted()) { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Authentication verification failed: " + e.getMessage()); + } } } private boolean shouldSkipAuthentication(HttpServletRequest request) { String path = request.getServletPath(); - return path.contains("/api/v1/auth"); + return path.contains("/api/v1/auth") || path.contains("/ws"); } private UserDetails getUserDetailsFromAuthenticationSource(HttpServletRequest request) { diff --git a/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/controller/ZoomController.java b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/controller/ZoomController.java new file mode 100644 index 00000000..b5692862 --- /dev/null +++ b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/controller/ZoomController.java @@ -0,0 +1,24 @@ +package org.kaiteki.backend.integrations.modules.zoom.controller; + +import lombok.RequiredArgsConstructor; +import org.kaiteki.backend.integrations.modules.zoom.models.dto.CreateZoomMeetingDTO; +import org.kaiteki.backend.integrations.modules.zoom.models.dto.ZoomMeetingDTO; +import org.kaiteki.backend.integrations.modules.zoom.service.ZoomService; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/zoom") +public class ZoomController { + private final ZoomService zoomService; + + @PostMapping() + public ZoomMeetingDTO createMeetingDTO(@RequestBody CreateZoomMeetingDTO dto) { + return zoomService.createZoomMeeting(dto); + } + + @GetMapping("/{id}") + public ZoomMeetingDTO createMeetingDTO(@PathVariable Long id) { + return zoomService.getZoomMeetingById(id); + } +} diff --git a/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomGlobalDialInNumbersDTO.java b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomGlobalDialInNumbersDTO.java new file mode 100644 index 00000000..28a89a90 --- /dev/null +++ b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomGlobalDialInNumbersDTO.java @@ -0,0 +1,18 @@ +package org.kaiteki.backend.integrations.modules.zoom.models.api; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +@Data +public class ZoomGlobalDialInNumbersDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + private String country; + private String country_name; + private String city; + private String number; + private String type; +} diff --git a/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomInterpreterDTO.java b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomInterpreterDTO.java new file mode 100644 index 00000000..dc1426aa --- /dev/null +++ b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomInterpreterDTO.java @@ -0,0 +1,15 @@ +package org.kaiteki.backend.integrations.modules.zoom.models.api; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +@Data +public class ZoomInterpreterDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + public String email; + public String languages; +} diff --git a/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomMeetingObjectDTO.java b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomMeetingObjectDTO.java new file mode 100644 index 00000000..14353991 --- /dev/null +++ b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomMeetingObjectDTO.java @@ -0,0 +1,39 @@ +package org.kaiteki.backend.integrations.modules.zoom.models.api; + +import lombok.Builder; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +@Data +@Builder +public class ZoomMeetingObjectDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + private Long id; + private String uuid; + private String assistant_id; + private String host_email; + private String registration_url; + private String topic; + private Integer type; + private String start_time; + private Integer duration; + private String schedule_for; + private String timezone; + private String created_at; + private String password; + private String agenda; + private String start_url; + private String join_url; + private String h323_password; + private Integer pmi; + private ZoomMeetingRecurrenceDTO recurrence; + private List tracking_fields; + private List occurrences; + private ZoomMeetingSettingsDTO settings; + +} diff --git a/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomMeetingOccurrenceDTO.java b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomMeetingOccurrenceDTO.java new file mode 100644 index 00000000..2405beba --- /dev/null +++ b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomMeetingOccurrenceDTO.java @@ -0,0 +1,17 @@ +package org.kaiteki.backend.integrations.modules.zoom.models.api; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +@Data +public class ZoomMeetingOccurrenceDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + private String occurrence_id; + private String start_time; + private Integer duration; + private String status; +} diff --git a/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomMeetingRecurrenceDTO.java b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomMeetingRecurrenceDTO.java new file mode 100644 index 00000000..cdbdedd0 --- /dev/null +++ b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomMeetingRecurrenceDTO.java @@ -0,0 +1,20 @@ +package org.kaiteki.backend.integrations.modules.zoom.models.api; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +@Data +public class ZoomMeetingRecurrenceDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private Integer type; + private Integer repeat_interval; + private String weekly_days; + private Integer monthly_day; + private Integer monthly_week; + private Integer monthly_week_day; + private Integer end_times; + private String end_date_time; +} diff --git a/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomMeetingSettingsDTO.java b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomMeetingSettingsDTO.java new file mode 100644 index 00000000..42cdb7c0 --- /dev/null +++ b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomMeetingSettingsDTO.java @@ -0,0 +1,43 @@ +package org.kaiteki.backend.integrations.modules.zoom.models.api; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +@Data +public class ZoomMeetingSettingsDTO implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + private Boolean host_video; + private Boolean participant_video; + private Boolean cn_meeting; + private Boolean in_meeting; + private Boolean join_before_host; + private Boolean mute_upon_entry; + private Boolean watermark; + private Boolean use_pmi; + private Integer approval_type; + private Integer registration_type; + private String audio; + private String auto_recording; + private String alternative_hosts; + private Boolean close_registration; + private Boolean waiting_room; + List global_dial_in_countries; + List global_dial_in_numbers; + private Boolean registrants_email_notification; + private String contact_name; + private String contact_email; + private Boolean registrants_confirmation_email; + private Boolean meeting_authentication; + private String authentication_option; + private String authenticated_domains; + private String authentication_name; + private Boolean show_share_button; + private Boolean allow_multiple_devices; + private String encryption_type; + public List interpreters; +} diff --git a/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomMeetingTrackingFieldsDTO.java b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomMeetingTrackingFieldsDTO.java new file mode 100644 index 00000000..589e68cc --- /dev/null +++ b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/api/ZoomMeetingTrackingFieldsDTO.java @@ -0,0 +1,11 @@ +package org.kaiteki.backend.integrations.modules.zoom.models.api; + +import lombok.Data; + +@Data +public class ZoomMeetingTrackingFieldsDTO { + public String field; + public String value; + public Boolean visible; + +} diff --git a/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/dto/CreateZoomMeetingDTO.java b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/dto/CreateZoomMeetingDTO.java new file mode 100644 index 00000000..89d5482c --- /dev/null +++ b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/dto/CreateZoomMeetingDTO.java @@ -0,0 +1,16 @@ +package org.kaiteki.backend.integrations.modules.zoom.models.dto; + +import lombok.Builder; +import lombok.Data; + +import java.time.ZonedDateTime; + +@Data +@Builder +public class CreateZoomMeetingDTO { + private String title; + private String description; + private String password; + private String creatorEmail; + private ZonedDateTime startTime; +} diff --git a/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/dto/ZoomMeetingDTO.java b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/dto/ZoomMeetingDTO.java new file mode 100644 index 00000000..b6138ef1 --- /dev/null +++ b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/models/dto/ZoomMeetingDTO.java @@ -0,0 +1,19 @@ +package org.kaiteki.backend.integrations.modules.zoom.models.dto; + +import lombok.Builder; +import lombok.Data; + +import java.time.ZonedDateTime; + +@Data +@Builder +public class ZoomMeetingDTO { + private Long zoomMeetingId; + private String title; + private String description; + private String creatorEmail; + private ZonedDateTime startTime; + private ZonedDateTime createdDate; + private String joinUrl; + private String password; +} diff --git a/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/service/ZoomAPIService.java b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/service/ZoomAPIService.java new file mode 100644 index 00000000..78d4f9e5 --- /dev/null +++ b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/service/ZoomAPIService.java @@ -0,0 +1,93 @@ +package org.kaiteki.backend.integrations.modules.zoom.service; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.kaiteki.backend.integrations.modules.zoom.models.api.ZoomMeetingObjectDTO; +import org.kaiteki.backend.integrations.modules.zoom.models.api.ZoomMeetingSettingsDTO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.server.ResponseStatusException; + +import java.security.Key; +import java.util.Date; +import java.util.UUID; + +@Service +public class ZoomAPIService { + @Value("${integrations.zoom.client.id}") + private String zoomUserId; + @Value("${integrations.zoom.client.secret}") + private String zoomApiSecret; + + + public ZoomMeetingObjectDTO createMeeting(ZoomMeetingObjectDTO zoomMeetingObjectDTO) { + String apiUrl = "https://api.zoom.us/v2/users/" + zoomUserId + "/meetings"; + + ZoomMeetingSettingsDTO settingsDTO = new ZoomMeetingSettingsDTO(); + settingsDTO.setJoin_before_host(true); + settingsDTO.setParticipant_video(false); + settingsDTO.setHost_video(false); + settingsDTO.setAuto_recording("cloud"); + settingsDTO.setMute_upon_entry(true); + settingsDTO.setAllow_multiple_devices(false); + settingsDTO.setMeeting_authentication(false); + + zoomMeetingObjectDTO.setSettings(settingsDTO); + + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer " + generateZoomJWTToken()); + headers.add("content-type", "application/json"); + + HttpEntity httpEntity = new HttpEntity<>(zoomMeetingObjectDTO, headers); + ResponseEntity zEntity = restTemplate.exchange(apiUrl, HttpMethod.POST, httpEntity, ZoomMeetingObjectDTO.class); + + if(zEntity.getStatusCode().value() != 201) { + throw new ResponseStatusException(zEntity.getStatusCode(), "Error while creating zoom meeting"); + } + + return zEntity.getBody(); + } + + public ZoomMeetingObjectDTO getZoomMeetingById(Long zoomMeetingId) { + String getMeetingUrl = "https://api.zoom.us/v2/meetings/" + zoomMeetingId; + + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + + headers.add("Authorization", "Bearer " + generateZoomJWTToken()); + headers.add("content-type", "application/json"); + + HttpEntity requestEntity = new HttpEntity<>(headers); + ResponseEntity zoomEntityRes = restTemplate + .exchange(getMeetingUrl, HttpMethod.GET, requestEntity, ZoomMeetingObjectDTO.class); + + if(zoomEntityRes.getStatusCode().value() != 200) { + throw new ResponseStatusException(zoomEntityRes.getStatusCode(), "Failed to get zoom meeting"); + } + + return zoomEntityRes.getBody(); + } + + private String generateZoomJWTToken() { + String id = UUID.randomUUID().toString().replace("-", ""); + + Date creation = new Date(System.currentTimeMillis()); + Date tokenExpiry = new Date(System.currentTimeMillis() + (1000 * 60)); + + Key key = Keys.hmacShaKeyFor(zoomApiSecret.getBytes()); + + return Jwts + .builder() + .id(id) + .issuer(zoomUserId) + .issuedAt(creation) + .expiration(tokenExpiry) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } +} diff --git a/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/service/ZoomService.java b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/service/ZoomService.java new file mode 100644 index 00000000..240e7bcf --- /dev/null +++ b/backend/src/main/java/org/kaiteki/backend/integrations/modules/zoom/service/ZoomService.java @@ -0,0 +1,48 @@ +package org.kaiteki.backend.integrations.modules.zoom.service; + +import lombok.RequiredArgsConstructor; +import org.kaiteki.backend.integrations.modules.zoom.models.dto.CreateZoomMeetingDTO; +import org.kaiteki.backend.integrations.modules.zoom.models.api.ZoomMeetingObjectDTO; +import org.kaiteki.backend.integrations.modules.zoom.models.dto.ZoomMeetingDTO; +import org.springframework.stereotype.Service; + +import java.time.ZonedDateTime; + +@Service +@RequiredArgsConstructor +public class ZoomService { + private final ZoomAPIService zoomAPIService; + + public ZoomMeetingDTO createZoomMeeting(CreateZoomMeetingDTO createDto) { + ZoomMeetingObjectDTO meetingBuilderDTO = ZoomMeetingObjectDTO.builder() + .agenda(createDto.getDescription()) + .topic(createDto.getTitle()) + .start_time(String.valueOf(createDto.getStartTime())) + .host_email(createDto.getCreatorEmail()) + .timezone("UTC") + .password(createDto.getPassword()) + .build(); + + ZoomMeetingObjectDTO response = zoomAPIService.createMeeting(meetingBuilderDTO); + + return convertToDTO(response); + } + + public ZoomMeetingDTO getZoomMeetingById(Long id) { + ZoomMeetingObjectDTO response = zoomAPIService.getZoomMeetingById(id); + return convertToDTO(response); + } + + public ZoomMeetingDTO convertToDTO(ZoomMeetingObjectDTO objectDTO) { + return ZoomMeetingDTO.builder() + .zoomMeetingId(objectDTO.getId()) + .joinUrl(objectDTO.getJoin_url()) + .password(objectDTO.getPassword()) + .title(objectDTO.getTopic()) + .startTime(ZonedDateTime.parse(objectDTO.getStart_time())) + .creatorEmail(objectDTO.getHost_email()) + .description(objectDTO.getAgenda()) + .createdDate(ZonedDateTime.parse(objectDTO.getCreated_at())) + .build(); + } +} diff --git a/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/controller/ChatMessagesController.java b/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/controller/ChatMessagesController.java new file mode 100644 index 00000000..b0ca423d --- /dev/null +++ b/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/controller/ChatMessagesController.java @@ -0,0 +1,44 @@ +package org.kaiteki.backend.teams.modules.chats.controller; + +import lombok.RequiredArgsConstructor; +import org.kaiteki.backend.teams.modules.chats.models.dto.*; +import org.kaiteki.backend.teams.modules.chats.services.ChatMessagesService; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +@RestController +@RequestMapping("api/v1/chats") +@RequiredArgsConstructor +public class ChatMessagesController { + private final ChatMessagesService chatMessagesService; + + @GetMapping("/{chatRoomId}/messages") + public List getMessagesByChatRoomId(@PathVariable Long chatRoomId) { + return chatMessagesService.getMessagesByChatRoomId(chatRoomId); + } + + @DeleteMapping("/{teamId}/{chatRoomId}/messages/{messageId}") + public void deleteMessage(@PathVariable Long teamId, @PathVariable Long chatRoomId, @PathVariable String messageId) { + chatMessagesService.deleteMessage(teamId, chatRoomId, messageId); + } + + @PutMapping("/{chatRoomId}/messages/{messageId}") + public void updateMessage(@PathVariable Long chatRoomId, + @PathVariable String messageId, + @RequestBody UpdateMessageDTO dto) { + chatMessagesService.updateMessage(chatRoomId, messageId, dto); + } + + @PostMapping("/{chatRoomId}/messages/send") + public void updateMessage(@PathVariable Long chatRoomId, + @RequestBody CreateMessageDTO dto) { + chatMessagesService.sendMessage(chatRoomId, dto); + } + + @PostMapping("/{teamId}/{chatRoomId}/messages/read") + public void readAllMessages(@PathVariable Long teamId, @PathVariable Long chatRoomId) { + chatMessagesService.readAllMessages(teamId, chatRoomId); + } +} diff --git a/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/controller/ChatsController.java b/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/controller/ChatsController.java index a9ff9690..312b8f7c 100644 --- a/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/controller/ChatsController.java +++ b/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/controller/ChatsController.java @@ -35,28 +35,6 @@ public ChatRoomsDTO getChatRooms(@RequestParam Long teamId, @PathVariable Long c return chatRoomsService.getChatRoomDTO(teamId, chatRoomId); } - @GetMapping("/{chatRoomId}/messages") - public List getMessagesByChatRoomId(@PathVariable Long chatRoomId) { - return chatRoomsService.getMessagesByChatRoomId(chatRoomId); - } - - @DeleteMapping("/{teamId}/{chatRoomId}/messages/{messageId}") - public void deleteMessage(@PathVariable Long teamId, @PathVariable Long chatRoomId, @PathVariable String messageId) { - chatRoomsService.deleteMessage(teamId, chatRoomId, messageId); - } - - @PutMapping("/{chatRoomId}/messages/{messageId}") - public void updateMessage(@PathVariable Long chatRoomId, - @PathVariable String messageId, - @RequestBody UpdateMessageDTO dto) { - chatRoomsService.updateMessage(chatRoomId, messageId, dto); - } - - @PostMapping("/{teamId}/{chatRoomId}/messages/read") - public void updateMessage(@PathVariable Long teamId, @PathVariable Long chatRoomId) { - chatRoomsService.readAllMessages(teamId, chatRoomId); - } - @PutMapping("/{chatRoomId}") public void updateChatRooms(@RequestParam Long teamId, @PathVariable Long chatRoomId, diff --git a/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/controller/ChatsSocketController.java b/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/controller/ChatsSocketController.java deleted file mode 100644 index b4fef6b0..00000000 --- a/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/controller/ChatsSocketController.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.kaiteki.backend.teams.modules.chats.controller; - -import lombok.RequiredArgsConstructor; -import org.kaiteki.backend.teams.modules.chats.services.ChatRoomsService; -import org.kaiteki.backend.teams.modules.chats.models.dto.CreateMessageDTO; -import org.springframework.messaging.handler.annotation.DestinationVariable; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.simp.annotation.SubscribeMapping; -import org.springframework.stereotype.Controller; - -@Controller -@RequiredArgsConstructor -public class ChatsSocketController { - private final ChatRoomsService chatRoomsService; - - @MessageMapping("/chat/{chatRoomId}/message/send") - public void sendMessage(@DestinationVariable Long chatRoomId, CreateMessageDTO createDto) { - chatRoomsService.sendMessage(chatRoomId, createDto); - } - - @SubscribeMapping("/queue/chat/{chatRoomId}/messages") - public void getRealtimeMessages() {} -} diff --git a/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/models/dto/TeamsChatNotificationDTO.java b/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/models/dto/TeamsChatNotificationDTO.java new file mode 100644 index 00000000..c453fefa --- /dev/null +++ b/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/models/dto/TeamsChatNotificationDTO.java @@ -0,0 +1,16 @@ +package org.kaiteki.backend.teams.modules.chats.models.dto; + +import lombok.Builder; +import lombok.Data; +import org.kaiteki.backend.teams.modules.chats.models.enums.TeamsChatNotificationType; + +import java.time.ZonedDateTime; + +@Data +@Builder +public class TeamsChatNotificationDTO { + private Long teamId; + private Long chatRoomId; + private TeamsChatNotificationType type; + private ZonedDateTime timestamp; +} diff --git a/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/models/enums/TeamsChatNotificationType.java b/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/models/enums/TeamsChatNotificationType.java new file mode 100644 index 00000000..88dc9ca3 --- /dev/null +++ b/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/models/enums/TeamsChatNotificationType.java @@ -0,0 +1,9 @@ +package org.kaiteki.backend.teams.modules.chats.models.enums; + +public enum TeamsChatNotificationType { + NEW_MESSAGE, + DELETE_MESSAGE, + UPDATE_MESSAGE, + ADD_MEMBER, + DELETE_MEMBER, +} diff --git a/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/services/ChatMessagesService.java b/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/services/ChatMessagesService.java index ac3c1ef1..c176515f 100644 --- a/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/services/ChatMessagesService.java +++ b/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/services/ChatMessagesService.java @@ -1,7 +1,10 @@ package org.kaiteki.backend.teams.modules.chats.services; -import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.kaiteki.backend.teams.model.entity.Teams; +import org.kaiteki.backend.teams.modules.chats.models.dto.TeamsChatNotificationDTO; +import org.kaiteki.backend.teams.modules.chats.models.entity.ChatRooms; +import org.kaiteki.backend.teams.modules.chats.models.enums.TeamsChatNotificationType; import org.kaiteki.backend.teams.modules.chats.repository.ChatMessagesRepository; import org.kaiteki.backend.teams.modules.chats.models.dto.ChatMessageDTO; import org.kaiteki.backend.teams.modules.chats.models.dto.CreateMessageDTO; @@ -13,11 +16,13 @@ import org.kaiteki.backend.teams.model.entity.TeamMembers; import org.kaiteki.backend.teams.service.TeamMembersService; import org.kaiteki.backend.users.models.enitities.Users; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.http.HttpStatus; +import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.server.ResponseStatusException; @@ -28,11 +33,79 @@ @Service -@RequiredArgsConstructor -class ChatMessagesService { - private final ChatMessagesRepository chatMessagesRepository; - private final TeamMembersService teamMembersService; - private final MongoTemplate mongoTemplate; +public class ChatMessagesService { + private ChatMessagesRepository chatMessagesRepository; + private TeamMembersService teamMembersService; + private MongoTemplate mongoTemplate; + private SimpMessagingTemplate simpMessagingTemplate; + private ChatRoomsService chatRoomsService; + + @Autowired + public void setTeamMembersService(TeamMembersService teamMembersService) { + this.teamMembersService = teamMembersService; + } + + @Autowired + public void setChatMessagesRepository(ChatMessagesRepository chatMessagesRepository) { + this.chatMessagesRepository = chatMessagesRepository; + } + + @Autowired + public void setChatRoomsService(ChatRoomsService chatRoomsService) { + this.chatRoomsService = chatRoomsService; + } + + @Autowired + public void setMongoTemplate(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + @Autowired + public void setSimpMessagingTemplate(SimpMessagingTemplate simpMessagingTemplate) { + this.simpMessagingTemplate = simpMessagingTemplate; + } + + @Transactional + public void sendMessage(Long chatRoomId, CreateMessageDTO dto) { + ChatRooms chatRoom = chatRoomsService.getChatRoom(chatRoomId); + Teams currentTeam = chatRoom.getTeam(); + + ChatMessages chatMessage = createChatMessage(chatRoomId, dto); + ChatMessageDTO createdChatMessage = convertToDTO(chatMessage); + + TeamsChatNotificationDTO teamsChatNotificationDTO = TeamsChatNotificationDTO.builder() + .teamId(currentTeam.getId()) + .chatRoomId(currentTeam.getId()) + .type(TeamsChatNotificationType.NEW_MESSAGE) + .timestamp(ZonedDateTime.now()) + .build(); + + simpMessagingTemplate.convertAndSend("/chats/" + chatRoomId + "/messages", createdChatMessage); + simpMessagingTemplate.convertAndSend("/chats/teams/" + currentTeam.getId() + "/notifications", teamsChatNotificationDTO); + + } + + @Transactional + public void deleteMessage(Long teamId, Long chatRoomId, String messageId) { + deleteMessage(teamId, messageId); + ChatRooms chatRoom = chatRoomsService.getChatRoom(chatRoomId); + Teams currentTeam = chatRoom.getTeam(); + + ChatMessageDTO dto = ChatMessageDTO.builder() + .eventType(ChatMessagesEventType.DELETE) + .id(messageId) + .build(); + + TeamsChatNotificationDTO teamsChatNotificationDTO = TeamsChatNotificationDTO.builder() + .teamId(currentTeam.getId()) + .chatRoomId(currentTeam.getId()) + .type(TeamsChatNotificationType.DELETE_MESSAGE) + .timestamp(ZonedDateTime.now()) + .build(); + + simpMessagingTemplate.convertAndSend("/chats/" + chatRoomId + "/messages", dto); + simpMessagingTemplate.convertAndSend("/chats/teams/" + currentTeam.getId() + "/notifications", teamsChatNotificationDTO); + } @Transactional public ChatMessages createChatMessage(Long chatRoomId, CreateMessageDTO dto) { @@ -65,6 +138,30 @@ public ChatMessages getChatMessageById(String id) { .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Message not found")); } + @Transactional + public void updateMessage(Long chatRoomId, String messageId, UpdateMessageDTO updateMessageDTO) { + ChatRooms chatRoom = chatRoomsService.getChatRoom(chatRoomId); + Teams currentTeam = chatRoom.getTeam(); + + ChatMessages updatedChatMessage = updateMessage(messageId, updateMessageDTO); + + ChatMessageDTO dto = ChatMessageDTO.builder() + .eventType(ChatMessagesEventType.UPDATE) + .content(updatedChatMessage.getContent()) + .id(messageId) + .build(); + + TeamsChatNotificationDTO teamsChatNotificationDTO = TeamsChatNotificationDTO.builder() + .teamId(currentTeam.getId()) + .chatRoomId(currentTeam.getId()) + .type(TeamsChatNotificationType.UPDATE_MESSAGE) + .timestamp(ZonedDateTime.now()) + .build(); + + simpMessagingTemplate.convertAndSend("/chats/" + chatRoomId + "/messages", dto); + simpMessagingTemplate.convertAndSend("/chats/teams/" + currentTeam.getId() + "/notifications", teamsChatNotificationDTO); + } + @Transactional public ChatMessages updateMessage(String messageId, UpdateMessageDTO dto) { ChatMessages message = getChatMessageById(messageId); diff --git a/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/services/ChatRoomsService.java b/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/services/ChatRoomsService.java index 1ae5cb8e..da7dbe31 100644 --- a/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/services/ChatRoomsService.java +++ b/backend/src/main/java/org/kaiteki/backend/teams/modules/chats/services/ChatRoomsService.java @@ -15,6 +15,7 @@ import org.kaiteki.backend.teams.model.entity.Teams; import org.kaiteki.backend.teams.service.TeamMembersService; import org.kaiteki.backend.teams.service.TeamsService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import org.springframework.http.HttpStatus; @@ -29,13 +30,31 @@ import static java.util.Objects.isNull; @Service -@RequiredArgsConstructor public class ChatRoomsService { - private final ChatRoomsRepository chatRoomsRepository; - private final TeamsService teamsService; - private final TeamMembersService teamMembersService; - private final SimpMessagingTemplate simpMessagingTemplate; - private final ChatMessagesService chatMessagesService; + private ChatRoomsRepository chatRoomsRepository; + private TeamsService teamsService; + private TeamMembersService teamMembersService; + private ChatMessagesService chatMessagesService; + + @Autowired + public void setTeamsService(TeamsService teamsService) { + this.teamsService = teamsService; + } + + @Autowired + public void setTeamMembersService(TeamMembersService teamMembersService) { + this.teamMembersService = teamMembersService; + } + + @Autowired + public void setChatMessagesService(ChatMessagesService chatMessagesService) { + this.chatMessagesService = chatMessagesService; + } + + @Autowired + public void setChatRoomsRepository(ChatRoomsRepository chatRoomsRepository) { + this.chatRoomsRepository = chatRoomsRepository; + } public List getChatRooms(Long teamId, ChatRoomsFilter filter) { TeamMembers currentMember = teamMembersService.getCurrentTeamMember(teamId); @@ -63,6 +82,11 @@ private static Specification getContainsMemberSpecification(TeamMembe }; } + public ChatRooms getChatRoom(Long chatRoomId) { + return chatRoomsRepository.findById(chatRoomId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat room not found")); + } + public ChatRoomsDTO getChatRoomDTO(Long teamId, Long chatRoomId) { TeamMembers currentMember = teamMembersService.getCurrentTeamMember(teamId); @@ -260,42 +284,4 @@ public void deleteChatRoom(Long teamId, Long chatRoomId) { chatRoomsRepository.delete(chatRoom); } - - public void sendMessage(Long chatRoomId, CreateMessageDTO dto) { - ChatMessages chatMessage = chatMessagesService.createChatMessage(chatRoomId, dto); - ChatMessageDTO responseDto = chatMessagesService.convertToDTO(chatMessage); - - simpMessagingTemplate.convertAndSend("/queue/chat/" + chatRoomId + "/messages", responseDto); - } - - public void deleteMessage(Long teamId, Long chatRoomId, String messageId) { - chatMessagesService.deleteMessage(teamId, messageId); - - ChatMessageDTO dto = ChatMessageDTO.builder() - .eventType(ChatMessagesEventType.DELETE) - .id(messageId) - .build(); - - simpMessagingTemplate.convertAndSend("/queue/chat/" + chatRoomId + "/messages", dto); - } - - public void updateMessage(Long chatRoomId, String messageId, UpdateMessageDTO updateMessageDTO) { - ChatMessages updatedChatMessage = chatMessagesService.updateMessage(messageId, updateMessageDTO); - - ChatMessageDTO dto = ChatMessageDTO.builder() - .eventType(ChatMessagesEventType.UPDATE) - .content(updatedChatMessage.getContent()) - .id(messageId) - .build(); - - simpMessagingTemplate.convertAndSend("/queue/chat/" + chatRoomId + "/messages", dto); - } - - public void readAllMessages(Long teamId, Long chatRoomId) { - chatMessagesService.readAllMessages(teamId, chatRoomId); - } - - public List getMessagesByChatRoomId(Long chatRoomId) { - return chatMessagesService.getMessagesByChatRoomId(chatRoomId); - } } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index b10f1a39..6cc63b1a 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -167,5 +167,9 @@ integrations: id: ${SPOTIFY_CLIENT_ID} secret: ${SPOTIFY_CLIENT_SECRET} redirect-url: ${SPOTIFY_REDIRECT_URL} + zoom: + client: + id: ${ZOOM_CLIENT_ID} + secret: ${ZOOM_CLIENT_SECRET} telegram: bot-url: ${TELEGRAM_BOT_URL} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index eed7df42..5201f173 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -54,7 +54,8 @@ "ngx-quill": "^25.1.1", "ngx-skeleton-loader": "^9.0.0", "quill": "^2.0.0-rc.0", - "rxjs": "~7.8.0", + "rxjs": "^7.8.1", + "socket.io-client": "^4.7.5", "tslib": "^2.3.0", "uuid": "^9.0.1", "zone.js": "~0.14.3" @@ -73,7 +74,6 @@ "@types/jasmine": "~5.1.0", "@types/node": "^18.18.0", "@types/quill": "^2.0.14", - "@types/sockjs-client": "^1.5.4", "@typescript-eslint/eslint-plugin": "7.2.0", "@typescript-eslint/parser": "7.2.0", "eslint": "^8.57.0", diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 8758c7c5..851d5469 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -11,6 +11,7 @@ import { authInterceptorProviders } from './auth/services/auth-interceptor.servi import { LandingModule } from './landing/landing.module' import { LandingLayoutModule } from './shared/layouts/landing-layout/landing-layout.module' import { PrimaryLayoutModule } from './shared/layouts/primary-layout/primary-layout.module' +import { RxStompService, rxStompServiceFactory } from './shared/services/rx-stomp.service' import { SharedModule } from './shared/shared.module' @NgModule({ @@ -26,6 +27,10 @@ import { SharedModule } from './shared/shared.module' LandingModule, ], bootstrap: [AppComponent], - providers: [authInterceptorProviders, provideCharts(withDefaultRegisterables())], + providers: [ + authInterceptorProviders, + provideCharts(withDefaultRegisterables()), + { provide: RxStompService, useFactory: rxStompServiceFactory }, + ], }) export class AppModule {} diff --git a/frontend/src/app/auth/pages/login/login.component.scss b/frontend/src/app/auth/pages/login/login.component.scss index 8fff04e6..4510d037 100644 --- a/frontend/src/app/auth/pages/login/login.component.scss +++ b/frontend/src/app/auth/pages/login/login.component.scss @@ -11,9 +11,10 @@ mat-form-field { &__left { position: relative; width: 100%; - max-width: 500px; + max-width: 40%; min-height: 100%; background: $primary; + border-right: 1px solid $border-color-dark; @media screen and (max-width: $md) { display: none; diff --git a/frontend/src/app/auth/pages/sign-up/sign-up.component.scss b/frontend/src/app/auth/pages/sign-up/sign-up.component.scss index 4371c1ba..9e1d018a 100644 --- a/frontend/src/app/auth/pages/sign-up/sign-up.component.scss +++ b/frontend/src/app/auth/pages/sign-up/sign-up.component.scss @@ -11,9 +11,10 @@ mat-form-field { &__left { position: relative; width: 100%; - max-width: 500px; + max-width: 40%; min-height: 100%; background: $primary; + border-right: 1px solid $border-color-dark; @media screen and (max-width: $md) { display: none; diff --git a/frontend/src/app/kaizen/pages/kaizen-home/kaizen-home.component.html b/frontend/src/app/kaizen/pages/kaizen-home/kaizen-home.component.html index 518a4d94..aa33ea00 100644 --- a/frontend/src/app/kaizen/pages/kaizen-home/kaizen-home.component.html +++ b/frontend/src/app/kaizen/pages/kaizen-home/kaizen-home.component.html @@ -44,7 +44,7 @@

- {{ response.value }} + {{ getFormattedResponse(response.value) }}

diff --git a/frontend/src/app/kaizen/pages/kaizen-home/kaizen-home.component.scss b/frontend/src/app/kaizen/pages/kaizen-home/kaizen-home.component.scss index 041605c3..8421372b 100644 --- a/frontend/src/app/kaizen/pages/kaizen-home/kaizen-home.component.scss +++ b/frontend/src/app/kaizen/pages/kaizen-home/kaizen-home.component.scss @@ -86,6 +86,10 @@ #f5f5f5, #e8f3ff ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ + + &-text { + white-space: pre-wrap; + } } } diff --git a/frontend/src/app/kaizen/pages/kaizen-home/kaizen-home.component.ts b/frontend/src/app/kaizen/pages/kaizen-home/kaizen-home.component.ts index fc4905bd..4c6397ba 100644 --- a/frontend/src/app/kaizen/pages/kaizen-home/kaizen-home.component.ts +++ b/frontend/src/app/kaizen/pages/kaizen-home/kaizen-home.component.ts @@ -111,4 +111,13 @@ export class KaizenHomeComponent implements OnInit, OnDestroy { isVoiceAssistantMode() { return this.currentMode === KAIZEN_MODES.VOICE } + + getFormattedResponse(text: string) { + if (this.currentMode === KAIZEN_MODES.CHATBOT) { + const formattedText = text.split('<|assistant|>\n')[1] || '' + return formattedText.replace(/(^\n|\n$|\n(?=[0-9]\.|\t))/g, '\n').trimStart() + } + + return text + } } diff --git a/frontend/src/app/shared/components/image-slider/image-slider.component.html b/frontend/src/app/shared/components/image-slider/image-slider.component.html index 32cbabec..8df3c17a 100644 --- a/frontend/src/app/shared/components/image-slider/image-slider.component.html +++ b/frontend/src/app/shared/components/image-slider/image-slider.component.html @@ -1,12 +1,8 @@ - - -
-
-
+
diff --git a/frontend/src/app/shared/components/image-slider/image-slider.component.scss b/frontend/src/app/shared/components/image-slider/image-slider.component.scss index 914fab82..aec636b8 100644 --- a/frontend/src/app/shared/components/image-slider/image-slider.component.scss +++ b/frontend/src/app/shared/components/image-slider/image-slider.component.scss @@ -1,20 +1,8 @@ -:host { - display: block; +.slide { width: 100%; - - .slides { - width: 100%; - height: 700px; - overflow: hidden; - position: relative; - - .slide { - width: 100%; - height: 100%; - background-size: cover; - background-position: center; - background-repeat: no-repeat; - position: relative; - } - } + height: 100%; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + position: relative; } diff --git a/frontend/src/app/shared/services/rx-stomp.service.ts b/frontend/src/app/shared/services/rx-stomp.service.ts index 1597359a..dfd9ca9d 100644 --- a/frontend/src/app/shared/services/rx-stomp.service.ts +++ b/frontend/src/app/shared/services/rx-stomp.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core' import { RxStomp, RxStompConfig } from '@stomp/rx-stomp' -const rxStompConfigrurations: RxStompConfig = { +const rxStompConfigurations: RxStompConfig = { brokerURL: 'ws://localhost:8080/ws', heartbeatIncoming: 0, heartbeatOutgoing: 20000, @@ -23,7 +23,7 @@ export class RxStompService extends RxStomp { export function rxStompServiceFactory() { const rxStomp = new RxStompService() - rxStomp.configure(rxStompConfigrurations) + rxStomp.configure(rxStompConfigurations) rxStomp.activate() return rxStomp } diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 8299a220..90584c17 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -14,7 +14,6 @@ import { PageHeaderComponent } from './components/page-header/page-header.compon import { PaginatorComponent } from './components/paginator/paginator.component' import { MaterialModule } from './material/mat.module' import { WithLoadingPipe } from './pipes/with-loading.pipe' -import { RxStompService, rxStompServiceFactory } from './services/rx-stomp.service' import { ButtonComponent } from './ui/button/button.component' import { IconComponent } from './ui/icon/icon.component' @@ -53,6 +52,6 @@ import { IconComponent } from './ui/icon/icon.component' BaseChartDirective, ImageSliderComponent, ], - providers: [{ provide: RxStompService, useFactory: rxStompServiceFactory }], + providers: [], }) export class SharedModule {} diff --git a/frontend/src/app/teams/components/layout/teams-layout.component.scss b/frontend/src/app/teams/components/layout/teams-layout.component.scss index 1c73c5e8..0b65c467 100644 --- a/frontend/src/app/teams/components/layout/teams-layout.component.scss +++ b/frontend/src/app/teams/components/layout/teams-layout.component.scss @@ -8,7 +8,7 @@ } .container { - height: calc(100vh - 128px); + height: calc(100vh - 133px); } .teams__error { diff --git a/frontend/src/app/teams/submodules/chats/components/chat-room/chat-room.component.html b/frontend/src/app/teams/submodules/chats/components/chat-room/chat-room.component.html index 9e011dfd..0c34c87c 100644 --- a/frontend/src/app/teams/submodules/chats/components/chat-room/chat-room.component.html +++ b/frontend/src/app/teams/submodules/chats/components/chat-room/chat-room.component.html @@ -1,92 +1,93 @@ -@if (currentChat$ | async; as selectedChat) { - @if (currentTeamMember$ | async; as currentTeamMember) { -
-
-
- @if (includeBackButton) { +
+ @if (currentChat$ | async; as selectedChat) { + @if (currentTeamMember$ | async; as currentTeamMember) { +
+
+
+ @if (includeBackButton) { + + } +
+

{{ selectedChat.name }}

+ @if (selectedChat.type === 'GROUP') { +

{{ selectedChat.size }} Members

+ } +
+
+
- } -
-

{{ selectedChat.name }}

- @if (selectedChat.type === 'GROUP') { -

{{ selectedChat.size }} Members

- } -
-
-
- - - @if (selectedChat.type === 'GROUP') { + + @if (selectedChat.type === 'GROUP') { + + } + +
+
+ +
+
+ @if (messages$ | async; as messages) { + @for (message of messages; track message.id; let i = $index; let last = $last) { + + {{ last ? scrollToBottom() : '' }} + } } +
+
+ +
+
+ - +
-
-
- @for ( - message of messages; - track messageTrackBy(i, message); - let i = $index; - let last = $last - ) { - - {{ last ? scrollToBottom() : '' }} - } -
-
-
-
- - -
-
-
+ } } -} + diff --git a/frontend/src/app/teams/submodules/chats/components/chat-room/chat-room.component.scss b/frontend/src/app/teams/submodules/chats/components/chat-room/chat-room.component.scss index 914dc0fb..03d38b79 100644 --- a/frontend/src/app/teams/submodules/chats/components/chat-room/chat-room.component.scss +++ b/frontend/src/app/teams/submodules/chats/components/chat-room/chat-room.component.scss @@ -1,11 +1,17 @@ @import 'vars'; .chats { + &__wrapper { + height: 100%; + padding: 16px; + } + &__content { + display: flex; + flex-direction: column; height: 100%; background: #ffffff; - margin: 16px 16px 0 0px; - border: 1px solid #e4dfdd; + border: 1px solid $border-color; border-radius: 8px; @media (max-width: $sm) { @@ -19,7 +25,7 @@ gap: 12px; align-items: center; padding: 14px 22px; - border-radius: 8px; + border-bottom: 1px solid $border-color; &-left { display: flex; @@ -58,8 +64,8 @@ &-wrapper { padding: 22px; overflow: hidden; - height: 80%; - background: #f8f9fb; + flex: 1; + background: #f8f8f8; } -ms-overflow-style: none; @@ -72,8 +78,8 @@ &__tabbar { background: #ffffff; - height: 10%; padding: 14px 22px; + border-top: 1px solid $border-color; @media (max-width: $sm) { position: fixed; @@ -85,6 +91,7 @@ &-wrapper { display: flex; align-items: center; + justify-content: center; gap: 22px; } diff --git a/frontend/src/app/teams/submodules/chats/components/chat-room/chat-room.component.ts b/frontend/src/app/teams/submodules/chats/components/chat-room/chat-room.component.ts index 4f9d39ef..4dd1c65c 100644 --- a/frontend/src/app/teams/submodules/chats/components/chat-room/chat-room.component.ts +++ b/frontend/src/app/teams/submodules/chats/components/chat-room/chat-room.component.ts @@ -18,7 +18,7 @@ import { TeamsService } from 'src/app/teams/services/teams.service' import { UpdateChatRoomDTO } from '../../models/chat-rooms.dto' import { ChatRooms } from '../../models/chat-rooms.model' import { CreateMessageDTO } from '../../models/message.dto' -import { ChatMessages, ChatMessagesType } from '../../models/message.model' +import { ChatMessagesType } from '../../models/message.model' import { ChatsService } from '../../services/chats.service' import { UpdateChatDialogComponent, @@ -37,14 +37,14 @@ export class ChatRoomComponent implements OnInit, OnDestroy { @Input() includeBackButton = false currentChat$ = this.chatsService.currentChatRoom$ - messages: ChatMessages[] = [] + messages$ = this.chatsService.currentChatRoomMessages$ currentTeamMember$ = this.teamsService.currentTeamMember$ form = new FormGroup({ content: new FormControl('', [ Validators.required, - Validators.max(2048), - Validators.min(1), + Validators.maxLength(2048), + Validators.minLength(1), ]), }) @@ -67,12 +67,11 @@ export class ChatRoomComponent implements OnInit, OnDestroy { takeUntil(this.unsubscribe$), ) .subscribe(data => { - this.messages = data - this.cd.markForCheck() + this.chatsService.addMessage(data) }) this.chatsService - .subscribeRealTimeChatMessages() + .subscribeCurrentChatMessages() .pipe( catchError(err => { this.toastService.error('Failed to receive messages') @@ -82,11 +81,11 @@ export class ChatRoomComponent implements OnInit, OnDestroy { ) .subscribe(resp => { if (resp) { - this.messages.push(resp) - this.scrollToBottom() - this.cd.markForCheck() + this.chatsService.addMessage(resp) } }) + + this.scrollToBottom() } ngOnDestroy(): void { @@ -102,10 +101,6 @@ export class ChatRoomComponent implements OnInit, OnDestroy { } } - messageTrackBy(index: number, message: ChatMessages) { - return message.id - } - onSendMessage() { const { content } = this.form.getRawValue() @@ -122,8 +117,12 @@ export class ChatRoomComponent implements OnInit, OnDestroy { return EMPTY } + if (!content.trim()) { + return EMPTY + } + const dto: CreateMessageDTO = { - content: content, + content: content.trim(), type: ChatMessagesType.TEXT, senderId: member.id, } @@ -137,8 +136,7 @@ export class ChatRoomComponent implements OnInit, OnDestroy { take(1), ) .subscribe(() => { - this.form.reset() - this.cd.markForCheck() + this.form.reset({ content: '' }) }) } diff --git a/frontend/src/app/teams/submodules/chats/components/chats-message/chats-message.component.scss b/frontend/src/app/teams/submodules/chats/components/chats-message/chats-message.component.scss index 7f7420a0..efb33aa0 100644 --- a/frontend/src/app/teams/submodules/chats/components/chats-message/chats-message.component.scss +++ b/frontend/src/app/teams/submodules/chats/components/chats-message/chats-message.component.scss @@ -67,7 +67,7 @@ } &__body { - background: $primary-accent; + background: $primary; color: #ffff; } } diff --git a/frontend/src/app/teams/submodules/chats/components/chats-sidebar/chats-sidebar.component.html b/frontend/src/app/teams/submodules/chats/components/chats-sidebar/chats-sidebar.component.html index 6c21d43d..e53b5c6e 100644 --- a/frontend/src/app/teams/submodules/chats/components/chats-sidebar/chats-sidebar.component.html +++ b/frontend/src/app/teams/submodules/chats/components/chats-sidebar/chats-sidebar.component.html @@ -1,102 +1,104 @@ - diff --git a/frontend/src/app/teams/submodules/chats/components/chats-sidebar/chats-sidebar.component.scss b/frontend/src/app/teams/submodules/chats/components/chats-sidebar/chats-sidebar.component.scss index 5e769d2e..a7065677 100644 --- a/frontend/src/app/teams/submodules/chats/components/chats-sidebar/chats-sidebar.component.scss +++ b/frontend/src/app/teams/submodules/chats/components/chats-sidebar/chats-sidebar.component.scss @@ -9,11 +9,15 @@ mat-form-field { height: 100%; overflow-y: auto; border-radius: 8px; - margin: 16px; background: #fff; color: $text-black; border: 1px solid $border-color; + &-wrapper { + height: 100%; + padding: 16px; + } + @media (min-width: $md) { width: 350px; } @@ -87,9 +91,11 @@ mat-form-field { width: 86%; display: flex; flex-direction: column; + align-items: start; } &__item-header { + width: 100%; display: flex; align-items: center; justify-content: space-between; diff --git a/frontend/src/app/teams/submodules/chats/models/message.model.ts b/frontend/src/app/teams/submodules/chats/models/message.model.ts index 0e02a6c9..44b22f88 100644 --- a/frontend/src/app/teams/submodules/chats/models/message.model.ts +++ b/frontend/src/app/teams/submodules/chats/models/message.model.ts @@ -13,6 +13,12 @@ export enum ChatMessagesEventType { DELETE = 'DELETE', } +export enum TeamsChatNotificationType { + NEW_MESSAGE = 'NEW_MESSAGE', + DELETE_MESSAGE = 'DELETE_MESSAGE', + UPDATE_MESSAGE = 'UPDATE_MESSAGE', +} + export interface ChatMessages { id: string content: string @@ -23,3 +29,10 @@ export interface ChatMessages { senderId: number senderFullName: string } + +export interface TeamsChatNotification { + teamId: number + chatRoomId: number + timestamp: Date + type: TeamsChatNotificationType +} diff --git a/frontend/src/app/teams/submodules/chats/pages/chats/chats.component.spec.ts b/frontend/src/app/teams/submodules/chats/pages/chats/chats.component.spec.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/src/app/teams/submodules/chats/pages/chats/chats.component.ts b/frontend/src/app/teams/submodules/chats/pages/chats/chats.component.ts index 2687e0cd..01aa54ba 100644 --- a/frontend/src/app/teams/submodules/chats/pages/chats/chats.component.ts +++ b/frontend/src/app/teams/submodules/chats/pages/chats/chats.component.ts @@ -1,6 +1,6 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy } from '@angular/core' +import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core' -import { Subject, take } from 'rxjs' +import { take } from 'rxjs' import { ChatsService } from '../../services/chats.service' @@ -10,8 +10,7 @@ import { ChatsService } from '../../services/chats.service' styleUrls: ['./chats.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ChatsComponent implements OnDestroy { - private destroy$ = new Subject() +export class ChatsComponent { currentChatRoom$ = this.chatsService.currentChatRoom$ constructor( @@ -19,13 +18,7 @@ export class ChatsComponent implements OnDestroy { private chatsService: ChatsService, ) {} - ngOnDestroy(): void { - this.destroy$.next() - this.destroy$.complete() - } - onSelectChat(chatId: number) { - // TODO: Check if current chat is already selected this.chatsService .getChatRoomById(chatId) .pipe(take(1)) diff --git a/frontend/src/app/teams/submodules/chats/services/chats-api.service.ts b/frontend/src/app/teams/submodules/chats/services/chats-api.service.ts index 305e92b3..b2e3d578 100644 --- a/frontend/src/app/teams/submodules/chats/services/chats-api.service.ts +++ b/frontend/src/app/teams/submodules/chats/services/chats-api.service.ts @@ -5,11 +5,13 @@ import { Observable } from 'rxjs' import { createQueryParams } from 'src/app/shared/utils/request-params.util' -import { ChatRoomsFilter, CreateChatRoomDTO, UpdateChatRoomDTO } from '../models/chat-rooms.dto' +import { ChatRoomsFilter, UpdateChatRoomDTO } from '../models/chat-rooms.dto' import { ChatRooms } from '../models/chat-rooms.model' -import { UpdateMessageDTO } from '../models/message.dto' +import { CreateMessageDTO, UpdateMessageDTO } from '../models/message.dto' import { ChatMessages } from '../models/message.model' +import { CreateChatRoomDTO } from './../models/chat-rooms.dto' + @Injectable({ providedIn: 'root', }) @@ -50,6 +52,10 @@ export class ChatsApiService { }) } + sendMessage(chatRoomId: number, dto: CreateMessageDTO): Observable { + return this.httpClient.post(`${this.baseUrl}/${chatRoomId}/messages/send`, dto) + } + deleteMessage(teamId: number, chatRoomId: number, messageId: string): Observable { return this.httpClient.delete( `${this.baseUrl}/${teamId}/${chatRoomId}/messages/${messageId}`, diff --git a/frontend/src/app/teams/submodules/chats/services/chats-messages-api.service.ts b/frontend/src/app/teams/submodules/chats/services/chats-messages-api.service.ts new file mode 100644 index 00000000..925bdb7d --- /dev/null +++ b/frontend/src/app/teams/submodules/chats/services/chats-messages-api.service.ts @@ -0,0 +1,46 @@ +import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' + +import { Observable } from 'rxjs' + +import { CreateMessageDTO, UpdateMessageDTO } from '../models/message.dto' +import { ChatMessages } from '../models/message.model' + +@Injectable({ + providedIn: 'root', +}) +export class ChatsMessagesApiService { + private readonly baseUrl: string = '/api/v1/chats' + + constructor(private httpClient: HttpClient) {} + + getHistoryMessages(chatRoomId: number) { + return this.httpClient.get(`${this.baseUrl}/${chatRoomId}/messages`) + } + + sendMessage(chatRoomId: number, dto: CreateMessageDTO): Observable { + return this.httpClient.post(`${this.baseUrl}/${chatRoomId}/messages/send`, dto) + } + + deleteMessage(teamId: number, chatRoomId: number, messageId: string): Observable { + return this.httpClient.delete( + `${this.baseUrl}/${teamId}/${chatRoomId}/messages/${messageId}`, + ) + } + + updateMessage( + teamId: number, + chatRoomId: number, + messageId: string, + updateMessageDTO: UpdateMessageDTO, + ): Observable { + return this.httpClient.put( + `${this.baseUrl}/${teamId}/${chatRoomId}/messages/${messageId}`, + updateMessageDTO, + ) + } + + readAllMessages(chatRoomId: number): Observable { + return this.httpClient.post(`${this.baseUrl}/${chatRoomId}/messages/read`, {}) + } +} diff --git a/frontend/src/app/teams/submodules/chats/services/chats-messages.service.ts b/frontend/src/app/teams/submodules/chats/services/chats-messages.service.ts index f015d6e4..a56cee14 100644 --- a/frontend/src/app/teams/submodules/chats/services/chats-messages.service.ts +++ b/frontend/src/app/teams/submodules/chats/services/chats-messages.service.ts @@ -1,50 +1,8 @@ import { Injectable } from '@angular/core' -import { map } from 'rxjs' - -import { RxStompService } from 'src/app/shared/services/rx-stomp.service' - -import { CreateMessageDTO } from '../models/message.dto' -import { ChatMessages } from '../models/message.model' - @Injectable({ providedIn: 'root', }) export class ChatsMessagesService { - constructor(private rxStompService: RxStompService) {} - - sendMessage(chatRoomId: number, dto: CreateMessageDTO) { - this.rxStompService.publish({ - destination: `/app/chat/${chatRoomId}/message/send`, - body: JSON.stringify(dto), - }) - } - - receiveMessages(chatRoomId: number) { - return this.rxStompService.watch(`/queue/chat/${chatRoomId}/messages`).pipe( - map(response => { - const data = JSON.parse(response.body) as object - if (this.isChatMessageType(data)) { - return data - } - - return null - }), - ) - } - - private isChatMessageType(object: object): object is ChatMessages { - if ( - 'id' in object && - 'content' in object && - 'status' in object && - 'messageType' in object && - 'eventType' in object && - 'sentDate' in object - ) { - return true - } - - return false - } + constructor() {} } diff --git a/frontend/src/app/teams/submodules/chats/services/chats.service.ts b/frontend/src/app/teams/submodules/chats/services/chats.service.ts index 83eeffc8..5bd75cfd 100644 --- a/frontend/src/app/teams/submodules/chats/services/chats.service.ts +++ b/frontend/src/app/teams/submodules/chats/services/chats.service.ts @@ -1,15 +1,18 @@ import { Injectable } from '@angular/core' -import { BehaviorSubject, EMPTY, Subject, combineLatest, switchMap, throwError } from 'rxjs' +import { BehaviorSubject, Subject, combineLatest, map, switchMap, throwError } from 'rxjs' + +import { RxStompService } from 'src/app/shared/services/rx-stomp.service' import { TeamsService } from 'src/app/teams/services/teams.service' import { ChatRoomsFilter, CreateChatRoomDTO, UpdateChatRoomDTO } from '../models/chat-rooms.dto' import { ChatRooms } from '../models/chat-rooms.model' import { CreateMessageDTO, UpdateMessageDTO } from '../models/message.dto' +import { ChatMessages, TeamsChatNotification } from '../models/message.model' import { ChatsApiService } from './chats-api.service' -import { ChatsMessagesService } from './chats-messages.service' +import { ChatsMessagesApiService } from './chats-messages-api.service' @Injectable({ providedIn: 'root', @@ -17,152 +20,234 @@ import { ChatsMessagesService } from './chats-messages.service' export class ChatsService { private refetchChatsSubject = new Subject() private currentChatRoomSubject = new BehaviorSubject(null) + private currentChatRoomMessagesSubject = new BehaviorSubject([]) refetchChats$ = this.refetchChatsSubject.asObservable() currentChatRoom$ = this.currentChatRoomSubject.asObservable() + currentChatRoomMessages$ = this.currentChatRoomMessagesSubject.asObservable() constructor( private teamsService: TeamsService, private chatsApiService: ChatsApiService, - private chatsMessagesService: ChatsMessagesService, + private rxStompService: RxStompService, + private chatsMessagesApiService: ChatsMessagesApiService, ) {} - setCurrentChat(chatRoom: ChatRooms | null) { - this.currentChatRoomSubject.next(chatRoom) + subscribeCurrentChatMessages() { + return this.currentChatRoom$.pipe( + switchMap(chat => { + if (chat) { + return this.subscribeToMessages(chat.id) + } + + return throwError(() => Error('No current chat room')) + }), + ) } - getChatRooms(filter: ChatRoomsFilter) { - return this.teamsService.currentTeam$.pipe( - switchMap(team => { - if (team) { - return this.chatsApiService.getChatRooms(team.id, filter) + sendMessageByCurrentChat(dto: CreateMessageDTO) { + return this.currentChatRoom$.pipe( + switchMap(chat => { + if (chat) { + return this.chatsMessagesApiService.sendMessage(chat.id, dto) } - return throwError(() => Error('No current team')) + return throwError(() => Error('No current chat room')) }), ) } - getChatRoomById(chartRoomId: number) { - return this.teamsService.currentTeam$.pipe( - switchMap(team => { - if (team) { - return this.chatsApiService.getChatRoomById(team.id, chartRoomId) + updateMessage(messageId: string, dto: UpdateMessageDTO) { + return combineLatest([this.teamsService.currentTeam$, this.currentChatRoom$]).pipe( + switchMap(([team, chat]) => { + if (chat && team) { + return this.chatsMessagesApiService.updateMessage(team.id, chat.id, messageId, dto) } - return throwError(() => Error('No current team')) + return throwError(() => Error('No current chat room or team')) }), ) } - createChatRoom(dto: CreateChatRoomDTO) { - return this.teamsService.currentTeam$.pipe( - switchMap(team => { - if (team) { - return this.chatsApiService.createChatRoom(team.id, dto) + getHistoryMessages() { + return this.currentChatRoom$.pipe( + switchMap(chat => { + if (chat) { + return this.chatsMessagesApiService.getHistoryMessages(chat.id) } - return throwError(() => Error('No current team')) + return throwError(() => Error('No current chat room')) }), ) } - updateChatRoom(chatRoomId: number, dto: UpdateChatRoomDTO) { - return this.teamsService.currentTeam$.pipe( - switchMap(team => { - if (team) { - return this.chatsApiService.updateChatRoom(team.id, chatRoomId, dto) + deleteMessage(messageId: string) { + return combineLatest([this.teamsService.currentTeam$, this.currentChatRoom$]).pipe( + switchMap(([team, chat]) => { + if (chat && team) { + return this.chatsMessagesApiService.deleteMessage(team.id, chat.id, messageId) } - return throwError(() => Error('No current team')) + return throwError(() => Error('No current chat room or team')) }), ) } - deleteChatRoom(chatRoomId: number) { + readAllCurrentMessages() { + return this.currentChatRoom$.pipe( + switchMap(chat => { + if (chat) { + return this.chatsMessagesApiService.readAllMessages(chat.id) + } + + return throwError(() => Error('No current chat room')) + }), + ) + } + + subscribeToMessages(chatRoomId: number) { + return this.rxStompService.watch(`/chats/${chatRoomId}/messages`).pipe( + map(response => { + const data = JSON.parse(response.body) as object + if (this.isChatMessageType(data)) { + return data + } + + return null + }), + ) + } + + private isChatMessageType(object: object): object is ChatMessages { + if ( + 'id' in object && + 'content' in object && + 'status' in object && + 'messageType' in object && + 'eventType' in object && + 'sentDate' in object + ) { + return true + } + + return false + } + + subscribeCurrentTeamChatsNotifications() { return this.teamsService.currentTeam$.pipe( switchMap(team => { if (team) { - return this.chatsApiService.deleteChatRoom(team.id, chatRoomId) + return this.subscribeTeamNotifications(team.id) } - return throwError(() => Error('No current team')) + return throwError(() => Error('No current team room')) }), ) } - getHistoryMessages() { - return this.currentChatRoom$.pipe( - switchMap(chat => { - if (chat) { - return this.chatsApiService.getHistoryMessages(chat.id) + subscribeTeamNotifications(teamId: number) { + return this.rxStompService.watch(`/chats/teams/${teamId}/notifications`).pipe( + map(response => { + const data = JSON.parse(response.body) as object + if (this.isTeamChatNotificationType(data)) { + return data } - return throwError(() => Error('No current chat room')) + return null }), ) } - subscribeRealTimeChatMessages() { - return this.currentChatRoom$.pipe( - switchMap(chat => { - if (chat) { - return this.chatsMessagesService.receiveMessages(chat.id) + private isTeamChatNotificationType(object: object): object is TeamsChatNotification { + if ('teamId' in object && 'chatRoomId' in object && 'timestamp' in object && 'type' in object) { + return true + } + + return false + } + + setCurrentChat(chatRoom: ChatRooms | null) { + if (chatRoom) { + if (chatRoom.id !== this.currentChatRoomSubject.value?.id) { + this.currentChatRoomSubject.next(chatRoom) + this.currentChatRoomMessagesSubject.next([]) + } + } else { + this.currentChatRoomSubject.next(null) + this.currentChatRoomMessagesSubject.next([]) + } + } + + addMessage(message: ChatMessages | ChatMessages[]) { + if (Array.isArray(message)) { + this.currentChatRoomMessagesSubject.next([ + ...this.currentChatRoomMessagesSubject.value, + ...message, + ]) + } else { + this.currentChatRoomMessagesSubject.next([ + ...this.currentChatRoomMessagesSubject.value, + message, + ]) + } + } + + getChatRooms(filter: ChatRoomsFilter) { + return this.teamsService.currentTeam$.pipe( + switchMap(team => { + if (team) { + return this.chatsApiService.getChatRooms(team.id, filter) } - return throwError(() => Error('No current chat room')) + return throwError(() => Error('No current team')) }), ) } - sendMessageByCurrentChat(dto: CreateMessageDTO) { - return this.currentChatRoom$.pipe( - switchMap(chat => { - if (chat) { - this.chatsMessagesService.sendMessage(chat.id, dto) - return EMPTY + getChatRoomById(chartRoomId: number) { + return this.teamsService.currentTeam$.pipe( + switchMap(team => { + if (team) { + return this.chatsApiService.getChatRoomById(team.id, chartRoomId) } - return throwError(() => Error('No current chat room')) + return throwError(() => Error('No current team')) }), ) } - updateMessage(messageId: string, dto: UpdateMessageDTO) { - return combineLatest([this.teamsService.currentTeam$, this.currentChatRoom$]).pipe( - switchMap(([team, chat]) => { - if (chat && team) { - this.chatsApiService.updateMessage(team.id, chat.id, messageId, dto) - return EMPTY + createChatRoom(dto: CreateChatRoomDTO) { + return this.teamsService.currentTeam$.pipe( + switchMap(team => { + if (team) { + return this.chatsApiService.createChatRoom(team.id, dto) } - return throwError(() => Error('No current chat room or team')) + return throwError(() => Error('No current team')) }), ) } - deleteMessage(messageId: string) { - return combineLatest([this.teamsService.currentTeam$, this.currentChatRoom$]).pipe( - switchMap(([team, chat]) => { - if (chat && team) { - this.chatsApiService.deleteMessage(team.id, chat.id, messageId) - return EMPTY + updateChatRoom(chatRoomId: number, dto: UpdateChatRoomDTO) { + return this.teamsService.currentTeam$.pipe( + switchMap(team => { + if (team) { + return this.chatsApiService.updateChatRoom(team.id, chatRoomId, dto) } - return throwError(() => Error('No current chat room or team')) + return throwError(() => Error('No current team')) }), ) } - readAllMessages() { - return this.currentChatRoom$.pipe( - switchMap(chat => { - if (chat) { - this.chatsApiService.readAllMessages(chat.id) - return EMPTY + deleteChatRoom(chatRoomId: number) { + return this.teamsService.currentTeam$.pipe( + switchMap(team => { + if (team) { + return this.chatsApiService.deleteChatRoom(team.id, chatRoomId) } - return throwError(() => Error('No current chat room')) + return throwError(() => Error('No current team')) }), ) } diff --git a/frontend/src/app/teams/submodules/dashboard/components/dashboard-toolbar/dashboard-toolbar.component.ts b/frontend/src/app/teams/submodules/dashboard/components/dashboard-toolbar/dashboard-toolbar.component.ts index 68a1a2ed..4cc53e24 100644 --- a/frontend/src/app/teams/submodules/dashboard/components/dashboard-toolbar/dashboard-toolbar.component.ts +++ b/frontend/src/app/teams/submodules/dashboard/components/dashboard-toolbar/dashboard-toolbar.component.ts @@ -26,6 +26,7 @@ export class DashboardToolbarComponent { const dialogRef = this.dialog.open(DashboardInviteDialogComponent, { minWidth: '30%', + maxWidth: '500px', }) dialogRef.afterClosed().pipe(take(1)).subscribe() diff --git a/frontend/src/app/teams/submodules/dashboard/components/dialogs/dashboard-invite-dialog/dashboard-invite-dialog.component.html b/frontend/src/app/teams/submodules/dashboard/components/dialogs/dashboard-invite-dialog/dashboard-invite-dialog.component.html index 73fcb918..ee686c32 100644 --- a/frontend/src/app/teams/submodules/dashboard/components/dialogs/dashboard-invite-dialog/dashboard-invite-dialog.component.html +++ b/frontend/src/app/teams/submodules/dashboard/components/dialogs/dashboard-invite-dialog/dashboard-invite-dialog.component.html @@ -26,16 +26,12 @@
- + diff --git a/frontend/src/app/teams/submodules/dashboard/components/dialogs/dashboard-invite-dialog/dashboard-invite-dialog.component.scss b/frontend/src/app/teams/submodules/dashboard/components/dialogs/dashboard-invite-dialog/dashboard-invite-dialog.component.scss index 17d93613..24233dfa 100644 --- a/frontend/src/app/teams/submodules/dashboard/components/dialogs/dashboard-invite-dialog/dashboard-invite-dialog.component.scss +++ b/frontend/src/app/teams/submodules/dashboard/components/dialogs/dashboard-invite-dialog/dashboard-invite-dialog.component.scss @@ -22,14 +22,13 @@ mat-form-field { &__actions { display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-start; } &__content { margin-top: 8px; display: flex; align-items: center; - max-width: 400px; } &__link-container { diff --git a/frontend/src/app/teams/submodules/dashboard/components/dialogs/dashboard-invite-dialog/dashboard-invite-dialog.component.ts b/frontend/src/app/teams/submodules/dashboard/components/dialogs/dashboard-invite-dialog/dashboard-invite-dialog.component.ts index b5cabe26..7f74160c 100644 --- a/frontend/src/app/teams/submodules/dashboard/components/dialogs/dashboard-invite-dialog/dashboard-invite-dialog.component.ts +++ b/frontend/src/app/teams/submodules/dashboard/components/dialogs/dashboard-invite-dialog/dashboard-invite-dialog.component.ts @@ -24,7 +24,7 @@ export class DashboardInviteDialogComponent { private clipboard: Clipboard, ) {} - onSubmit() { + onCloseClick() { this.dialogRef.close() } diff --git a/frontend/src/proxy.conf.json b/frontend/src/proxy.conf.json index 6134bc7d..60c725ce 100644 --- a/frontend/src/proxy.conf.json +++ b/frontend/src/proxy.conf.json @@ -10,7 +10,7 @@ "changeOrigin": true }, "/ws/**": { - "target": "ws://localhost:8080", + "target": "ws://localhost:8080/ws", "secure": false, "changeOrigin": true, "ws": true diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 0feb9783..2826427b 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3097,11 +3097,6 @@ "@types/node" "*" "@types/send" "*" -"@types/sockjs-client@^1.5.4": - version "1.5.4" - resolved "https://registry.yarnpkg.com/@types/sockjs-client/-/sockjs-client-1.5.4.tgz#2c0b6aadf0cfeb49cd210e4c59995a6608fbd8ea" - integrity sha512-zk+uFZeWyvJ5ZFkLIwoGA/DfJ+pYzcZ8eH4H/EILCm2OBZyHH6Hkdna1/UWL/CFruh5wj6ES7g75SvUB0VsH5w== - "@types/sockjs@^0.3.33": version "0.3.36" resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" @@ -4530,6 +4525,17 @@ end-of-stream@^1.4.1: dependencies: once "^1.4.0" +engine.io-client@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.3.tgz#4cf6fa24845029b238f83c628916d9149c399bc5" + integrity sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.11.0" + xmlhttprequest-ssl "~2.0.0" + engine.io-parser@~5.2.1: version "5.2.2" resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49" @@ -7507,7 +7513,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@7.8.1, rxjs@^7.8.1, rxjs@~7.8.0: +rxjs@7.8.1, rxjs@^7.8.1: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== @@ -7759,6 +7765,16 @@ socket.io-adapter@~2.5.2: debug "~4.3.4" ws "~8.11.0" +socket.io-client@^4.7.5: + version "4.7.5" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.5.tgz#919be76916989758bdc20eec63f7ee0ae45c05b7" + integrity sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.5.2" + socket.io-parser "~4.2.4" + socket.io-parser@~4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" @@ -7927,7 +7943,16 @@ streamroller@^3.1.5: debug "^4.3.4" fs-extra "^8.1.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7959,7 +7984,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7973,6 +7998,13 @@ strip-ansi@^3.0.0: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -8563,7 +8595,7 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -8581,6 +8613,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -8610,6 +8651,11 @@ xhr2@^0.2.0: resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.2.1.tgz#4e73adc4f9cfec9cbd2157f73efdce3a5f108a93" integrity sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw== +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== + y18n@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" diff --git a/kaizen/src/services/chatbot_service.py b/kaizen/src/services/chatbot_service.py index dec82d9e..db28f9d4 100644 --- a/kaizen/src/services/chatbot_service.py +++ b/kaizen/src/services/chatbot_service.py @@ -1,5 +1,5 @@ -import transformers import torch +import transformers model_name = "TinyLlama/TinyLlama-1.1B-Chat-v1.0" tokenizer = transformers.AutoTokenizer.from_pretrained(model_name) @@ -13,6 +13,7 @@ device = 'cuda' if torch.cuda.is_available() else 'cpu' model.to(device) +print(device) def generate_response(prompt): formatted_prompt = f"<|system|>\nYou are a chatbot named Kaizen who can help with anything!\n<|user|>\n{prompt}\n<|assistant|>\n" diff --git a/kaizen/test/gputest.py b/kaizen/test/gputest.py index 4a99e027..1bdcb76f 100644 --- a/kaizen/test/gputest.py +++ b/kaizen/test/gputest.py @@ -1,15 +1,9 @@ -import GPUtil -GPUtil.getAvailable() +import subprocess -import torch -use_cuda = torch.cuda.is_available() -if use_cuda: - print('__CUDNN VERSION:', torch.backends.cudnn.version()) - print('__Number CUDA Devices:', torch.cuda.device_count()) - print('__CUDA Device Name:',torch.cuda.get_device_name(0)) - print('__CUDA Device Total Memory [GB]:',torch.cuda.get_device_properties(0).total_memory/1e9) - -device = torch.device("cuda" if use_cuda else "cpu") -print("Device: ",device) - -print("Test finished") \ No newline at end of file +try: + gpu_info_result = subprocess.run(["nvidia-smi"], capture_output=True, text=True, check=True) + gpu_info = gpu_info_result.stdout +except subprocess.CalledProcessError: + print('Not connected to a GPU') +else: + print(gpu_info)