From 80b92a842501f65ccfcf93a136e0e926905c71e0 Mon Sep 17 00:00:00 2001 From: Thibaut Mouton Date: Tue, 16 Apr 2024 11:27:40 +0200 Subject: [PATCH] fix():compiling errors --- .github/workflows/maven.yml | 2 +- .github/workflows/npm.yml | 2 +- README.md | 13 +- backend/pom.xml | 41 +- .../mercure/config/HandShakeInterceptor.java | 2 +- .../config/JwtAuthenticationEntryPoint.java | 4 +- .../java/com/mercure/config/JwtWebConfig.java | 12 +- .../com/mercure/config/SecurityConfig.java | 106 +-- .../config/WebSocketSecurityConfig.java | 23 +- .../java/com/mercure/config/WsConfig.java | 16 +- .../com/mercure/controller/ApiController.java | 5 +- .../controller/AuthenticationController.java | 27 +- .../mercure/controller/MessageController.java | 25 + .../com/mercure/controller/WsController.java | 62 +- .../mercure/controller/WsFileController.java | 2 - .../com/mercure/dto/WrapperMessageDTO.java | 2 + .../com/mercure/dto/user/GroupWrapperDTO.java | 1 - .../java/com/mercure/entity/FileEntity.java | 2 +- .../java/com/mercure/entity/GroupEntity.java | 5 +- .../java/com/mercure/entity/GroupRoleKey.java | 4 +- .../java/com/mercure/entity/GroupUser.java | 6 +- .../com/mercure/entity/MessageEntity.java | 2 +- .../com/mercure/entity/MessageUserEntity.java | 5 +- .../com/mercure/entity/MessageUserKey.java | 4 +- .../java/com/mercure/entity/UserEntity.java | 5 +- .../java/com/mercure/mapper/GroupMapper.java | 2 +- .../java/com/mercure/model/UserModel.java | 1 - .../mercure/repository/GroupRepository.java | 7 +- .../mercure/repository/MessageRepository.java | 5 +- .../com/mercure/service/GroupService.java | 21 +- .../com/mercure/service/MessageService.java | 41 +- .../com/mercure/service/StorageService.java | 15 +- .../service/UserSeenMessageService.java | 7 - .../java/com/mercure/service/UserService.java | 2 +- .../com/mercure/utils/FileNameGenerator.java | 33 - .../src/main/resources/application.properties | 14 +- .../db/changelog/createFileBlobTable.xml | 8 +- .../db/changelog/createGroupTable.xml | 8 +- .../db/changelog/createMessageWsTable.xml | 8 +- .../db/changelog/createUserTable.xml | 8 +- frontend-web/.eslintrc.js | 39 + frontend-web/.eslintrc.json | 42 - frontend-web/.npmrc | 1 + frontend-web/.nvmrc | 1 + frontend-web/package.json | 4 + .../assets/icons/Sound-Amplitude-Frame.svg | 816 ------------------ .../public/assets/icons/landing_logo.svg | 32 + frontend-web/public/logo192.png | Bin 5347 -> 0 bytes frontend-web/public/logo512.png | Bin 9664 -> 0 bytes frontend-web/src/App.css | 96 --- frontend-web/src/App.tsx | 47 - .../create-group/create-group-component.tsx | 186 ++-- frontend-web/src/components/home.tsx | 87 +- .../src/components/login/login-component.tsx | 275 +++--- .../message/CreateMessageComponent.tsx | 165 ++++ .../components/partials/HeaderComponent.tsx | 132 +++ ...data-component.tsx => NoDataComponent.tsx} | 2 +- .../components/partials/alert-component.tsx | 62 +- .../components/partials/all-users-dialog.tsx | 3 +- .../partials/custom-material-textfield.tsx | 171 ++-- .../components/partials/footer-component.tsx | 2 +- .../components/partials/header-component.tsx | 160 ---- .../partials/loader/LoaderComponent.tsx | 16 + .../partials/video/active-video-call.tsx | 66 +- .../src/components/register/register-form.css | 4 +- .../src/components/register/register-user.tsx | 7 +- .../src/components/utils/enable-dark-mode.ts | 2 +- ...-component.tsx => CallWindowComponent.tsx} | 20 +- .../websocket/video-component-style.css | 3 - .../components/websocket/video-component.tsx | 460 +++++----- .../websocket/websocket-chat-component.tsx | 643 ++++++-------- .../websocket-group-actions-component.tsx | 44 +- .../websocket/websocket-groups-component.tsx | 112 +-- .../websocket/websocket-main-component.tsx | 73 +- frontend-web/src/config/websocket-config.ts | 29 +- frontend-web/src/context/AlertContext.tsx | 52 ++ frontend-web/src/context/AuthContext.tsx | 34 + frontend-web/src/context/WebsocketContext.tsx | 131 +++ frontend-web/src/context/auth-context.tsx | 51 -- frontend-web/src/context/loader-context.tsx | 26 +- frontend-web/src/context/ws-context.tsx | 130 --- frontend-web/src/index.css | 88 ++ frontend-web/src/index.tsx | 63 +- .../src/interface-contract/csrf/csrf.type.ts | 5 + .../wrapper-message-model.ts | 1 + frontend-web/src/service/http-main.service.ts | 20 + .../src/service/http-message.service.ts | 13 + frontend-web/src/service/http-service.ts | 172 ++-- .../src/utils/string-size-calculator.ts | 2 +- 89 files changed, 2169 insertions(+), 2979 deletions(-) create mode 100644 backend/src/main/java/com/mercure/controller/MessageController.java delete mode 100644 backend/src/main/java/com/mercure/utils/FileNameGenerator.java create mode 100644 frontend-web/.eslintrc.js delete mode 100644 frontend-web/.eslintrc.json create mode 100644 frontend-web/.npmrc create mode 100644 frontend-web/.nvmrc delete mode 100644 frontend-web/public/assets/icons/Sound-Amplitude-Frame.svg create mode 100644 frontend-web/public/assets/icons/landing_logo.svg delete mode 100644 frontend-web/public/logo192.png delete mode 100644 frontend-web/public/logo512.png delete mode 100644 frontend-web/src/App.css delete mode 100644 frontend-web/src/App.tsx create mode 100644 frontend-web/src/components/message/CreateMessageComponent.tsx create mode 100644 frontend-web/src/components/partials/HeaderComponent.tsx rename frontend-web/src/components/partials/{no-data-component.tsx => NoDataComponent.tsx} (89%) delete mode 100644 frontend-web/src/components/partials/header-component.tsx create mode 100644 frontend-web/src/components/partials/loader/LoaderComponent.tsx rename frontend-web/src/components/websocket/{call-window-component.tsx => CallWindowComponent.tsx} (78%) delete mode 100644 frontend-web/src/components/websocket/video-component-style.css create mode 100644 frontend-web/src/context/AlertContext.tsx create mode 100644 frontend-web/src/context/AuthContext.tsx create mode 100644 frontend-web/src/context/WebsocketContext.tsx delete mode 100644 frontend-web/src/context/auth-context.tsx delete mode 100644 frontend-web/src/context/ws-context.tsx create mode 100644 frontend-web/src/interface-contract/csrf/csrf.type.ts create mode 100644 frontend-web/src/service/http-main.service.ts create mode 100644 frontend-web/src/service/http-message.service.ts diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 25fb8eb..094ef7d 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -1,4 +1,4 @@ -name: maven +name: build-back on: push diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index 7de9240..9813166 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -1,4 +1,4 @@ -name: npm +name: build-front on: push diff --git a/README.md b/README.md index 030b957..56637a1 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,20 @@ +![maven build status](https://github.com/Thibaut-Mouton/react-spring-messenger-project/workflows/maven/badge.svg?branch=master) +![npm build status](https://github.com/Thibaut-Mouton/react-spring-messenger-project/workflows/npm/badge.svg?branch=master) +

React logo Spring boot logo

-# FastLiteMessage ![build status](https://github.com/Thibaut-Mouton/react-spring-messenger-project/workflows/maven/badge.svg?branch=master) +# FastLiteMessage Real time chat application group oriented built with React and Spring Boot. Talk with your friends, create and add users to conversation, send messages or images, set groups administrators and start video calls ! (coming soon) # Project Requirements * [JDK](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) 17 -* [NodeJS](https://nodejs.org/en/download/) v16.14.2 -* [ReactJS](https://reactjs.org/) v17 +* [NodeJS](https://nodejs.org/en/download/) v20.12.1 +* [ReactJS](https://reactjs.org/) v18 * [Material UI](https://mui.com/) v5.7.0 * [MySQL Server](https://www.mysql.com/) @@ -24,7 +27,7 @@ This will start 3 containers : MySQL, backend and frontend together. Liquibase * Luke * Steve ``` -Be sure that no other app is running on port 3000, 9090 or 3306 +Warning : Be sure that no other app is running on port 3000, 9090 or 3306 ``` # Project development set up @@ -33,7 +36,7 @@ Be sure that no other app is running on port 3000, 9090 or 3306 * You can disable Liquibase by setting ```spring.liquibase.enabled=false``` in ```application.properties```. * To try the project on localhost, check that nothing runs on ports 9090 (Spring Boot) and 3000 (React app) * You can edit ````spring.datasource```` in ```backend/src/main/resources/application.properties``` and ```username``` and ```password``` in ```backend/src/main/resources/liquibase.properties``` with your own MySQL login / password -* Create a database named "fastlitemessage" or you can also modify the name in the properties files mentioned just above. +* Create a database named "fastlitemessage_dev" or you can also modify the name in the properties files mentioned just above. ## Start backend * Go inside backend folder then type ```mvn spring-boot:run``` to launch backend. diff --git a/backend/pom.xml b/backend/pom.xml index fca05a8..95e15fc 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -1,13 +1,14 @@ - 4.0.0 org.springframework.boot spring-boot-starter-parent - 2.6.7 - + 3.2.4 + com.mercure messenger 1.3.0 @@ -40,50 +41,62 @@ org.springframework.boot spring-boot-starter-data-jpa - 2.6.7 + 3.2.4 org.springframework.boot spring-boot-starter-security + 3.2.4 com.google.guava guava - 32.0.0-jre + 33.1.0-jre + + + + jakarta.servlet + jakarta.servlet-api + 6.1.0-M2 + provided org.springframework.security spring-security-messaging + 6.2.3 org.springframework.boot spring-boot-starter-web + 3.2.4 - mysql - mysql-connector-java - 8.0.29 + com.mysql + mysql-connector-j + 8.3.0 org.liquibase liquibase-core + 4.27.0 org.springframework.boot spring-boot-starter-websocket + 3.2.4 com.google.code.gson gson - 2.9.0 + 2.10.1 @@ -95,24 +108,26 @@ org.springframework.boot spring-boot-starter-batch + 3.2.4 org.springframework.boot spring-boot-starter-cache + 3.2.4 org.projectlombok lombok - 1.18.24 + 1.18.32 provided org.liquibase liquibase-maven-plugin - 4.10.0 + 4.27.0 @@ -122,7 +137,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.0 + 3.13.0 17 @@ -137,7 +152,7 @@ org.liquibase liquibase-maven-plugin - 4.0.0 + 4.10.0 src/main/resources/liquibase.properties diff --git a/backend/src/main/java/com/mercure/config/HandShakeInterceptor.java b/backend/src/main/java/com/mercure/config/HandShakeInterceptor.java index 3b9dbe1..c9d487c 100644 --- a/backend/src/main/java/com/mercure/config/HandShakeInterceptor.java +++ b/backend/src/main/java/com/mercure/config/HandShakeInterceptor.java @@ -1,6 +1,7 @@ package com.mercure.config; import com.mercure.service.UserService; +import jakarta.servlet.http.HttpSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -14,7 +15,6 @@ import org.springframework.web.socket.WebSocketMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.server.HandshakeInterceptor; -import javax.servlet.http.HttpSession; import java.util.Map; @Configuration diff --git a/backend/src/main/java/com/mercure/config/JwtAuthenticationEntryPoint.java b/backend/src/main/java/com/mercure/config/JwtAuthenticationEntryPoint.java index a59808c..46ee0ac 100644 --- a/backend/src/main/java/com/mercure/config/JwtAuthenticationEntryPoint.java +++ b/backend/src/main/java/com/mercure/config/JwtAuthenticationEntryPoint.java @@ -1,11 +1,11 @@ package com.mercure.config; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.Serializable; diff --git a/backend/src/main/java/com/mercure/config/JwtWebConfig.java b/backend/src/main/java/com/mercure/config/JwtWebConfig.java index 2b5d23c..0fbafe1 100644 --- a/backend/src/main/java/com/mercure/config/JwtWebConfig.java +++ b/backend/src/main/java/com/mercure/config/JwtWebConfig.java @@ -3,6 +3,11 @@ import com.mercure.service.CustomUserDetailsService; import com.mercure.utils.JwtUtil; import com.mercure.utils.StaticVariable; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; @@ -11,11 +16,6 @@ import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.WebUtils; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component @@ -28,7 +28,7 @@ public class JwtWebConfig extends OncePerRequestFilter { private JwtUtil jwtUtil; @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ServletException { String jwtToken = null; String username; Cookie cookie = WebUtils.getCookie(request, StaticVariable.SECURE_COOKIE); diff --git a/backend/src/main/java/com/mercure/config/SecurityConfig.java b/backend/src/main/java/com/mercure/config/SecurityConfig.java index 340f112..7858396 100644 --- a/backend/src/main/java/com/mercure/config/SecurityConfig.java +++ b/backend/src/main/java/com/mercure/config/SecurityConfig.java @@ -1,87 +1,63 @@ package com.mercure.config; -import com.mercure.service.CustomUserDetailsService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.security.web.csrf.CookieCsrfTokenRepository; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -import java.util.Arrays; -import java.util.List; +import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity -public class SecurityConfig extends WebSecurityConfigurerAdapter { - - @Autowired - private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; - - @Autowired - private JwtWebConfig jwtWebConfig; - - @Autowired - private CustomUserDetailsService customUserDetailsService; +public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } - - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder()); - } +// @Bean +// public CorsConfigurationSource corsConfigurationSource() { +// CorsConfiguration configuration = new CorsConfiguration(); +// configuration.setAllowedOrigins(List.of("http://localhost:3000")); +// configuration.setAllowedMethods(Arrays.asList("GET", "POST", "OPTIONS")); +// configuration.setAllowedMethods(List.of("*")); +// configuration.setAllowedHeaders(List.of("*")); +// configuration.setAllowCredentials(Boolean.TRUE); +// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); +// source.registerCorsConfiguration("/**", configuration); +// return source; +// } @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(List.of("http://localhost:3000")); - configuration.setAllowedMethods(Arrays.asList("GET", "POST")); - configuration.setAllowedHeaders(List.of("*")); - configuration.addAllowedOriginPattern("*"); - configuration.setAllowCredentials(Boolean.TRUE); - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - return source; + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .cors(Customizer.withDefaults()) + .authorizeHttpRequests((auth) -> auth.anyRequest().permitAll()); + return http.build(); } - @Override - protected void configure(HttpSecurity http) throws Exception { - http.cors().and() - .csrf() - .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) - .and() - .authorizeRequests() - .antMatchers("/api/auth").permitAll() - .antMatchers("/api/user/register").permitAll() - .antMatchers("/ws").permitAll() - .antMatchers("/static/**").permitAll() - .antMatchers("/images/**").permitAll() - .antMatchers("/").permitAll() - .anyRequest().authenticated() - .and() - .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) - .and() - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.STATELESS); - http.addFilterBefore(jwtWebConfig, UsernamePasswordAuthenticationFilter.class); - } +// protected void configure(HttpSecurity http) throws Exception { +// http.cors().and() +// .csrf() +// .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) +// .and() +// .authorizeRequests() +// .antMatchers("/api/auth").permitAll() +// .antMatchers("/api/user/register").permitAll() +// .antMatchers("/ws").permitAll() +// .antMatchers("/static/**").permitAll() +// .antMatchers("/images/**").permitAll() +// .antMatchers("/").permitAll() +// .anyRequest().authenticated() +// .and() +// .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) +// .and() +// .sessionManagement() +// .sessionCreationPolicy(SessionCreationPolicy.STATELESS); +// http.addFilterBefore(jwtWebConfig, UsernamePasswordAuthenticationFilter.class); +// } } diff --git a/backend/src/main/java/com/mercure/config/WebSocketSecurityConfig.java b/backend/src/main/java/com/mercure/config/WebSocketSecurityConfig.java index 6d91363..1c8b4db 100644 --- a/backend/src/main/java/com/mercure/config/WebSocketSecurityConfig.java +++ b/backend/src/main/java/com/mercure/config/WebSocketSecurityConfig.java @@ -1,20 +1,19 @@ package com.mercure.config; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry; -import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer; +import org.springframework.messaging.Message; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.config.annotation.web.socket.EnableWebSocketSecurity; +import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager; @Configuration -public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { +@EnableWebSocketSecurity +public class WebSocketSecurityConfig { - @Override - protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { - messages.simpDestMatchers("/ws/**").permitAll().anyMessage().permitAll(); - messages.simpDestMatchers("/wss/**").permitAll().anyMessage().permitAll(); - } - - @Override - protected boolean sameOriginDisabled() { - return true; + @Bean + public AuthorizationManager> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) { + messages.anyMessage().permitAll(); + return messages.build(); } } diff --git a/backend/src/main/java/com/mercure/config/WsConfig.java b/backend/src/main/java/com/mercure/config/WsConfig.java index f04b5cd..b4cd798 100644 --- a/backend/src/main/java/com/mercure/config/WsConfig.java +++ b/backend/src/main/java/com/mercure/config/WsConfig.java @@ -1,6 +1,5 @@ package com.mercure.config; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; @@ -11,17 +10,16 @@ @EnableWebSocketMessageBroker public class WsConfig implements WebSocketMessageBrokerConfigurer { - @Autowired - private HandShakeInterceptor handShakeInterceptor; - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/messenger", "/call").addInterceptors(handShakeInterceptor).setAllowedOrigins("http://localhost:3000").withSockJS(); + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/topic"); } @Override - public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.enableSimpleBroker("/queue", "/topic"); - registry.setApplicationDestinationPrefixes("/app"); + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/messenger").setAllowedOrigins("http://localhost:3000"); + registry.addEndpoint("/messenger", "/call") + .setAllowedOrigins("http://localhost:3000") + .withSockJS(); } } diff --git a/backend/src/main/java/com/mercure/controller/ApiController.java b/backend/src/main/java/com/mercure/controller/ApiController.java index 972cd44..0290cdf 100644 --- a/backend/src/main/java/com/mercure/controller/ApiController.java +++ b/backend/src/main/java/com/mercure/controller/ApiController.java @@ -13,6 +13,8 @@ import com.mercure.service.UserService; import com.mercure.utils.JwtUtil; import com.mercure.utils.StaticVariable; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -20,12 +22,9 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.util.WebUtils; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; import java.util.*; @RestController -@RequestMapping(value = "/api") public class ApiController { private final Logger log = LoggerFactory.getLogger(ApiController.class); diff --git a/backend/src/main/java/com/mercure/controller/AuthenticationController.java b/backend/src/main/java/com/mercure/controller/AuthenticationController.java index 3e63537..ef97741 100644 --- a/backend/src/main/java/com/mercure/controller/AuthenticationController.java +++ b/backend/src/main/java/com/mercure/controller/AuthenticationController.java @@ -1,7 +1,8 @@ package com.mercure.controller; import com.google.gson.Gson; -import com.mercure.dto.*; +import com.mercure.dto.AuthUserDTO; +import com.mercure.dto.JwtDTO; import com.mercure.dto.user.GroupDTO; import com.mercure.dto.user.InitUserDTO; import com.mercure.entity.GroupEntity; @@ -13,26 +14,26 @@ import com.mercure.service.UserService; import com.mercure.utils.JwtUtil; import com.mercure.utils.StaticVariable; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.DisabledException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.csrf.CsrfToken; import org.springframework.web.bind.annotation.*; import org.springframework.web.util.WebUtils; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; @RestController -@RequestMapping(value = "/api") +@CrossOrigin(allowCredentials = "true", origins = "http://localhost:3000") public class AuthenticationController { - @Autowired - private AuthenticationManager authenticationManager; + private final Logger log = LoggerFactory.getLogger(AuthenticationController.class); @Autowired private JwtUtil jwtTokenUtil; @@ -66,6 +67,7 @@ public AuthUserDTO createAuthenticationToken(@RequestBody JwtDTO authenticationR // 7 days jwtAuthToken.setMaxAge(7 * 24 * 60 * 60); response.addCookie(jwtAuthToken); + log.debug("User authenticated successfully"); return userMapper.toLightUserDTO(user); } @@ -80,6 +82,11 @@ public ResponseEntity fetchInformation(HttpServletResponse response) { return ResponseEntity.ok().build(); } + @GetMapping(value = "/csrf") + public CsrfToken getCsrfToken(CsrfToken token) { + return token; + } + @GetMapping(value = "/fetch") public InitUserDTO fetchInformation(HttpServletRequest request) { return userMapper.toUserDTO(getUserEntity(request)); @@ -87,7 +94,7 @@ public InitUserDTO fetchInformation(HttpServletRequest request) { private void authenticate(String username, String password) throws Exception { try { - authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); + //authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); } catch (DisabledException e) { throw new Exception("USER_DISABLED", e); } catch (BadCredentialsException e) { diff --git a/backend/src/main/java/com/mercure/controller/MessageController.java b/backend/src/main/java/com/mercure/controller/MessageController.java new file mode 100644 index 0000000..13dc6d8 --- /dev/null +++ b/backend/src/main/java/com/mercure/controller/MessageController.java @@ -0,0 +1,25 @@ +package com.mercure.controller; + +import com.mercure.dto.WrapperMessageDTO; +import com.mercure.service.MessageService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping(value = "/messages") +@CrossOrigin(allowCredentials = "true", origins = "http://localhost:3000") +public class MessageController { + + private final Logger log = LoggerFactory.getLogger(MessageController.class); + + @Autowired + private MessageService messageService; + + @GetMapping(value = "/group/{groupUrl}") + public WrapperMessageDTO fetchGroupMessages(@PathVariable String groupUrl) { + this.log.debug("Fetching messages from conversation"); + return this.messageService.getConversationMessage(groupUrl, -1); + } +} diff --git a/backend/src/main/java/com/mercure/controller/WsController.java b/backend/src/main/java/com/mercure/controller/WsController.java index 81ce5d9..b1e76d4 100644 --- a/backend/src/main/java/com/mercure/controller/WsController.java +++ b/backend/src/main/java/com/mercure/controller/WsController.java @@ -18,7 +18,6 @@ import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.annotation.SendToUser; import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @@ -29,7 +28,6 @@ import java.util.Map; @RestController -@CrossOrigin public class WsController { @Autowired @@ -69,23 +67,22 @@ public void mainChannel(InputTransportDTO dto, @Header("simpSessionId") String s case SEND_GROUP_MESSAGE: this.getAndSaveMessage(dto.getUserId(), dto.getGroupUrl(), dto.getMessage()); break; - case FETCH_GROUP_MESSAGES: - if (!dto.getGroupUrl().equals("")) { - int groupId = groupService.findGroupByUrl(dto.getGroupUrl()); - if (dto.getGroupUrl().equals("") || groupUserJoinService.checkIfUserIsAuthorizedInGroup(dto.getUserId(), groupId)) { - break; - } - WrapperMessageDTO messages = this.getConversationMessage(dto.getGroupUrl(), dto.getMessageId()); - OutputTransportDTO resMessages = new OutputTransportDTO(); - if (dto.getMessageId() == -1) { - resMessages.setAction(TransportActionEnum.FETCH_GROUP_MESSAGES); - } else { - resMessages.setAction(TransportActionEnum.ADD_CHAT_HISTORY); - } - resMessages.setObject(messages); - this.messagingTemplate.convertAndSend("/topic/user/" + dto.getUserId(), resMessages); - } - break; +// case FETCH_GROUP_MESSAGES: +// if (!dto.getGroupUrl().equals("")) { +// int groupId = groupService.findGroupByUrl(dto.getGroupUrl()); +// if (dto.getGroupUrl().equals("") || groupUserJoinService.checkIfUserIsAuthorizedInGroup(dto.getUserId(), groupId)) { +// break; +// } +// OutputTransportDTO resMessages = new OutputTransportDTO(); +// if (dto.getMessageId() == -1) { +// resMessages.setAction(TransportActionEnum.FETCH_GROUP_MESSAGES); +// } else { +// resMessages.setAction(TransportActionEnum.ADD_CHAT_HISTORY); +// } +// resMessages.setObject(messages); +// this.messagingTemplate.convertAndSend("/topic/user/" + dto.getUserId(), resMessages); +// } +// break; case MARK_MESSAGE_AS_SEEN: if (!"".equals(dto.getGroupUrl())) { int messageId = messageService.findLastMessageIdByGroupId(groupService.findGroupByUrl(dto.getGroupUrl())); @@ -243,31 +240,4 @@ public void wsCreateConversation(String payload) { Long id2 = createGroup.getId2(); groupService.createConversation(id1.intValue(), id2.intValue()); } - - /** - * Return history of group discussion - * - * @param url The group url to map - * @return List of message - */ - public WrapperMessageDTO getConversationMessage(String url, int messageId) { - WrapperMessageDTO wrapper = new WrapperMessageDTO(); - if (url != null) { - List messageDTOS = new ArrayList<>(); - int groupId = groupService.findGroupByUrl(url); - List newMessages = messageService.findByGroupId(groupId, messageId); - int lastMessageId = newMessages != null && newMessages.size() != 0 ? newMessages.get(0).getId() : 0; - List afterMessages = messageService.findByGroupId(groupId, lastMessageId); - if (newMessages != null) { - wrapper.setLastMessage(afterMessages != null && afterMessages.size() == 0); - newMessages.forEach(msg -> - messageDTOS.add(messageService - .createMessageDTO(msg.getId(), msg.getType(), msg.getUser_id(), msg.getCreatedAt().toString(), msg.getGroup_id(), msg.getMessage())) - ); - } - wrapper.setMessages(messageDTOS); - return wrapper; - } - return null; - } } diff --git a/backend/src/main/java/com/mercure/controller/WsFileController.java b/backend/src/main/java/com/mercure/controller/WsFileController.java index 1129d71..8a0e6ac 100644 --- a/backend/src/main/java/com/mercure/controller/WsFileController.java +++ b/backend/src/main/java/com/mercure/controller/WsFileController.java @@ -24,8 +24,6 @@ * API controller to handle file upload */ @RestController -@CrossOrigin -@RequestMapping("/api") public class WsFileController { private static Logger log = LoggerFactory.getLogger(WsFileController.class); diff --git a/backend/src/main/java/com/mercure/dto/WrapperMessageDTO.java b/backend/src/main/java/com/mercure/dto/WrapperMessageDTO.java index ed975d6..993cba9 100644 --- a/backend/src/main/java/com/mercure/dto/WrapperMessageDTO.java +++ b/backend/src/main/java/com/mercure/dto/WrapperMessageDTO.java @@ -15,5 +15,7 @@ public class WrapperMessageDTO { private boolean isLastMessage; + private String groupName; + private List messages; } diff --git a/backend/src/main/java/com/mercure/dto/user/GroupWrapperDTO.java b/backend/src/main/java/com/mercure/dto/user/GroupWrapperDTO.java index ab69fee..7e63f60 100644 --- a/backend/src/main/java/com/mercure/dto/user/GroupWrapperDTO.java +++ b/backend/src/main/java/com/mercure/dto/user/GroupWrapperDTO.java @@ -1,6 +1,5 @@ package com.mercure.dto.user; - import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/backend/src/main/java/com/mercure/entity/FileEntity.java b/backend/src/main/java/com/mercure/entity/FileEntity.java index 2eb43a9..0d811d6 100644 --- a/backend/src/main/java/com/mercure/entity/FileEntity.java +++ b/backend/src/main/java/com/mercure/entity/FileEntity.java @@ -1,12 +1,12 @@ package com.mercure.entity; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.CreationTimestamp; -import javax.persistence.*; import java.sql.Timestamp; @Entity diff --git a/backend/src/main/java/com/mercure/entity/GroupEntity.java b/backend/src/main/java/com/mercure/entity/GroupEntity.java index eeef92b..405c802 100644 --- a/backend/src/main/java/com/mercure/entity/GroupEntity.java +++ b/backend/src/main/java/com/mercure/entity/GroupEntity.java @@ -1,15 +1,14 @@ package com.mercure.entity; - import com.fasterxml.jackson.annotation.JsonIgnore; import com.mercure.utils.GroupTypeEnum; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.CreationTimestamp; -import javax.persistence.*; import java.io.Serializable; import java.sql.Timestamp; import java.util.HashSet; @@ -58,7 +57,7 @@ public GroupEntity(int id, String name, String url) { @JsonIgnore private Set userEntities = new HashSet<>(); - @OneToMany(mappedBy = "groupMapping", fetch = FetchType.EAGER) + @OneToMany(mappedBy = "groupUsers", fetch = FetchType.EAGER) @JsonIgnore private Set groupUsers = new HashSet<>(); } diff --git a/backend/src/main/java/com/mercure/entity/GroupRoleKey.java b/backend/src/main/java/com/mercure/entity/GroupRoleKey.java index 5324c64..50b6518 100644 --- a/backend/src/main/java/com/mercure/entity/GroupRoleKey.java +++ b/backend/src/main/java/com/mercure/entity/GroupRoleKey.java @@ -1,12 +1,12 @@ package com.mercure.entity; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import javax.persistence.Column; -import javax.persistence.Embeddable; import java.io.Serializable; import java.util.Objects; diff --git a/backend/src/main/java/com/mercure/entity/GroupUser.java b/backend/src/main/java/com/mercure/entity/GroupUser.java index b346de4..805f886 100644 --- a/backend/src/main/java/com/mercure/entity/GroupUser.java +++ b/backend/src/main/java/com/mercure/entity/GroupUser.java @@ -1,11 +1,11 @@ package com.mercure.entity; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import javax.persistence.*; import java.io.Serializable; import java.util.Objects; @@ -27,12 +27,12 @@ public class GroupUser implements Serializable { @ManyToOne @MapsId("groupId") @JoinColumn(name = "group_id") - GroupEntity groupMapping; + GroupEntity groupUsers; @ManyToOne @MapsId("userId") @JoinColumn(name = "user_id") - UserEntity userMapping; + UserEntity userEntities; private int role; diff --git a/backend/src/main/java/com/mercure/entity/MessageEntity.java b/backend/src/main/java/com/mercure/entity/MessageEntity.java index cf944cc..10d3fca 100644 --- a/backend/src/main/java/com/mercure/entity/MessageEntity.java +++ b/backend/src/main/java/com/mercure/entity/MessageEntity.java @@ -1,12 +1,12 @@ package com.mercure.entity; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.CreationTimestamp; -import javax.persistence.*; import java.sql.Timestamp; @Entity diff --git a/backend/src/main/java/com/mercure/entity/MessageUserEntity.java b/backend/src/main/java/com/mercure/entity/MessageUserEntity.java index 4fe2649..26fdb48 100644 --- a/backend/src/main/java/com/mercure/entity/MessageUserEntity.java +++ b/backend/src/main/java/com/mercure/entity/MessageUserEntity.java @@ -1,11 +1,14 @@ package com.mercure.entity; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.Table; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import javax.persistence.*; import java.io.Serializable; import java.util.Objects; diff --git a/backend/src/main/java/com/mercure/entity/MessageUserKey.java b/backend/src/main/java/com/mercure/entity/MessageUserKey.java index 9ea4cc6..e692536 100644 --- a/backend/src/main/java/com/mercure/entity/MessageUserKey.java +++ b/backend/src/main/java/com/mercure/entity/MessageUserKey.java @@ -1,12 +1,12 @@ package com.mercure.entity; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import javax.persistence.Column; -import javax.persistence.Embeddable; import java.io.Serializable; import java.util.Objects; diff --git a/backend/src/main/java/com/mercure/entity/UserEntity.java b/backend/src/main/java/com/mercure/entity/UserEntity.java index 5178955..50b8a18 100644 --- a/backend/src/main/java/com/mercure/entity/UserEntity.java +++ b/backend/src/main/java/com/mercure/entity/UserEntity.java @@ -1,5 +1,6 @@ package com.mercure.entity; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -7,7 +8,6 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; -import javax.persistence.*; import java.io.Serializable; import java.util.*; @@ -44,9 +44,6 @@ public UserEntity(int id, String firstName, String password) { @ManyToMany(fetch = FetchType.EAGER, mappedBy = "userEntities", cascade = CascadeType.ALL) private Set groupSet = new HashSet<>(); - @OneToMany(mappedBy = "groupMapping", fetch = FetchType.EAGER) - private Set groupUsers = new HashSet<>(); - @Column(name = "expiration_date") @Temporal(TemporalType.TIMESTAMP) private Date expiration_date; diff --git a/backend/src/main/java/com/mercure/mapper/GroupMapper.java b/backend/src/main/java/com/mercure/mapper/GroupMapper.java index 1c68457..db798d0 100644 --- a/backend/src/main/java/com/mercure/mapper/GroupMapper.java +++ b/backend/src/main/java/com/mercure/mapper/GroupMapper.java @@ -54,6 +54,6 @@ public GroupDTO toGroupDTO(GroupEntity grp, int userId) { } public GroupMemberDTO toGroupMemberDTO(GroupUser groupUser) { - return new GroupMemberDTO(groupUser.getUserMapping().getId(), groupUser.getUserMapping().getFirstName(), groupUser.getUserMapping().getLastName(), groupUser.getRole() == 1); + return new GroupMemberDTO(groupUser.getUserEntities().getId(), groupUser.getUserEntities().getFirstName(), groupUser.getUserEntities().getLastName(), groupUser.getRole() == 1); } } diff --git a/backend/src/main/java/com/mercure/model/UserModel.java b/backend/src/main/java/com/mercure/model/UserModel.java index 4ad3a93..334a2a2 100644 --- a/backend/src/main/java/com/mercure/model/UserModel.java +++ b/backend/src/main/java/com/mercure/model/UserModel.java @@ -4,7 +4,6 @@ import lombok.Setter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; - import java.util.Collection; @Setter diff --git a/backend/src/main/java/com/mercure/repository/GroupRepository.java b/backend/src/main/java/com/mercure/repository/GroupRepository.java index 727dfbf..e6364fd 100644 --- a/backend/src/main/java/com/mercure/repository/GroupRepository.java +++ b/backend/src/main/java/com/mercure/repository/GroupRepository.java @@ -1,12 +1,14 @@ package com.mercure.repository; import com.mercure.entity.GroupEntity; -import liquibase.pro.packaged.Q; +import com.mercure.entity.GroupUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface GroupRepository extends JpaRepository { @@ -16,6 +18,9 @@ public interface GroupRepository extends JpaRepository { @Query(value = "SELECT g.name FROM chat_group g WHERE g.url = :url", nativeQuery = true) String getGroupEntitiesBy(@Param(value = "url") String url); + @Query(value = "SELECT * FROM chat_group g WHERE g.url = :url", nativeQuery = true) + GroupEntity getGroupByUrl(@Param(value = "url") String url); + @Query(value = "SELECT g.url FROM chat_group g WHERE g.id = :id", nativeQuery = true) String getGroupUrlById(@Param(value = "id") Integer id); } diff --git a/backend/src/main/java/com/mercure/repository/MessageRepository.java b/backend/src/main/java/com/mercure/repository/MessageRepository.java index 9738938..6667f22 100644 --- a/backend/src/main/java/com/mercure/repository/MessageRepository.java +++ b/backend/src/main/java/com/mercure/repository/MessageRepository.java @@ -15,7 +15,10 @@ public interface MessageRepository extends JpaRepository @Query(value = "SELECT * FROM (SELECT * FROM message m WHERE m.msg_group_id=:id and ((:offset > 0 and m.id < :offset) or (:offset <= 0)) ORDER BY m.id DESC LIMIT 20)t order by id", nativeQuery = true) List findByGroupIdAndOffset(@Param(value = "id") int id, @Param(value = "offset") int offset); - @Query(value = "SELECT * FROM message m1 INNER JOIN (SELECT MAX(m.id) as id FROM message m GROUP BY m.msg_group_id) temp ON temp.id = m1.id WHERE msg_group_id = :idOfGroup", nativeQuery = true) + @Query(value = "SELECT * FROM (SELECT * FROM message m WHERE m.msg_group_id=:id ORDER BY m.id DESC LIMIT 20)t order by id", nativeQuery = true) + List findLastMessagesByGroupId(@Param(value = "id") int id); + + @Query(value = "SELECT * FROM message m1 INNER JOIN (SELECT MAX(m.id) as mId FROM message m GROUP BY m.msg_group_id) temp ON temp.mId = m1.id WHERE msg_group_id = :idOfGroup", nativeQuery = true) MessageEntity findLastMessageByGroupId(@Param(value = "idOfGroup") int groupId); @Query(value = "SELECT m1.id FROM message m1 INNER JOIN (SELECT MAX(m.id) as id FROM message m GROUP BY m.msg_group_id) temp ON temp.id = m1.id WHERE msg_group_id = :idOfGroup", nativeQuery = true) diff --git a/backend/src/main/java/com/mercure/service/GroupService.java b/backend/src/main/java/com/mercure/service/GroupService.java index 618ac3e..c402260 100644 --- a/backend/src/main/java/com/mercure/service/GroupService.java +++ b/backend/src/main/java/com/mercure/service/GroupService.java @@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import javax.swing.*; import java.util.*; import java.util.stream.Collectors; @@ -30,6 +31,10 @@ public int findGroupByUrl(String url) { return groupRepository.findGroupByUrl(url); } + public GroupEntity getGroupByUrl(String url) { + return groupRepository.getGroupByUrl(url); + } + public List getAllUsersIdByGroupUrl(String groupUrl) { int groupId = groupRepository.findGroupByUrl(groupUrl); List users = groupUserJoinService.findAllByGroupId(groupId); @@ -52,8 +57,8 @@ public GroupMemberDTO addUserToConversation(int userId, int groupId) { } UserEntity user = userService.findById(userId); GroupUser groupUser = new GroupUser(); - groupUser.setGroupMapping(groupEntity.orElse(null)); - groupUser.setUserMapping(user); + groupUser.setGroupUsers(groupEntity.orElse(null)); + groupUser.setUserEntities(user); groupUser.setGroupId(groupId); groupUser.setUserId(userId); groupUser.setRole(0); @@ -78,8 +83,8 @@ public GroupEntity createGroup(int userId, String name) { groupUser.setGroupId(savedGroup.getId()); groupUser.setUserId(userId); groupUser.setRole(1); - groupUser.setUserMapping(user); - groupUser.setGroupMapping(group); + groupUser.setUserEntities(user); + groupUser.setGroupUsers(group); groupUserJoinService.save(groupUser); return savedGroup; } @@ -103,15 +108,15 @@ public void createConversation(int id1, int id2) { groupUser1.setUserId(id1); groupUser1.setRole(0); - groupUser1.setUserMapping(user1); - groupUser1.setGroupMapping(groupEntity); + groupUser1.setUserEntities(user1); + groupUser1.setGroupUsers(groupEntity); GroupUser groupUser2 = new GroupUser(); groupUser2.setUserId(savedGroup.getId()); groupUser2.setGroupId(id2); groupUser2.setRole(0); - groupUser2.setUserMapping(user2); - groupUser2.setGroupMapping(groupEntity); + groupUser2.setUserEntities(user2); + groupUser2.setGroupUsers(groupEntity); groupUserJoinService.saveAll(Arrays.asList(groupUser1, groupUser2)); } } diff --git a/backend/src/main/java/com/mercure/service/MessageService.java b/backend/src/main/java/com/mercure/service/MessageService.java index 8e883a1..1c76551 100644 --- a/backend/src/main/java/com/mercure/service/MessageService.java +++ b/backend/src/main/java/com/mercure/service/MessageService.java @@ -2,6 +2,7 @@ import com.mercure.dto.MessageDTO; import com.mercure.dto.NotificationDTO; +import com.mercure.dto.WrapperMessageDTO; import com.mercure.entity.FileEntity; import com.mercure.entity.GroupEntity; import com.mercure.entity.MessageEntity; @@ -21,11 +22,14 @@ public class MessageService { private MessageRepository messageRepository; @Autowired - private UserService userService; + private MessageService messageService; @Autowired private GroupService groupService; + @Autowired + private UserService userService; + @Autowired private FileService fileService; @@ -56,9 +60,11 @@ public MessageEntity save(MessageEntity messageEntity) { } public List findByGroupId(int id, int offset) { - List list = messageRepository.findByGroupIdAndOffset(id, offset); - if (list.size() == 0) { - return new ArrayList<>(); + List list; + if (offset == -1) { + list = messageRepository.findByGroupIdAndOffset(id, offset); + } else { + list = messageRepository.findLastMessagesByGroupId(id); } return list; } @@ -164,4 +170,31 @@ public MessageDTO createNotificationMessageDTO(MessageEntity msg, int userId) { messageDTO.setMessageSeen(msg.getUser_id() == userId); return messageDTO; } + + /** + * Return history of group discussion + * + * @param url The group url to map + * @return List of message + */ + public WrapperMessageDTO getConversationMessage(String url, int messageId) { + WrapperMessageDTO wrapper = new WrapperMessageDTO(); + if (url != null) { + List messageDTOS = new ArrayList<>(); + GroupEntity group = groupService.getGroupByUrl(url); + List newMessages = messageService.findByGroupId(group.getId(), messageId); + if (newMessages != null) { + newMessages.forEach(msg -> + messageDTOS.add(messageService + .createMessageDTO(msg.getId(), msg.getType(), msg.getUser_id(), msg.getCreatedAt().toString(), msg.getGroup_id(), msg.getMessage())) + ); + } +// wrapper.setLastMessage(afterMessages != null && afterMessages.isEmpty()); + wrapper.setLastMessage(true); + wrapper.setMessages(messageDTOS); + wrapper.setGroupName(group.getName()); + return wrapper; + } + return null; + } } diff --git a/backend/src/main/java/com/mercure/service/StorageService.java b/backend/src/main/java/com/mercure/service/StorageService.java index 969a81b..cbc1ad7 100644 --- a/backend/src/main/java/com/mercure/service/StorageService.java +++ b/backend/src/main/java/com/mercure/service/StorageService.java @@ -1,8 +1,8 @@ package com.mercure.service; import com.mercure.entity.FileEntity; -import com.mercure.utils.FileNameGenerator; import com.mercure.utils.StaticVariable; +import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -11,25 +11,18 @@ import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import javax.annotation.PostConstruct; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.Objects; +import java.util.UUID; -/** - * @author https://attacomsian.com/blog/uploading-files-spring-boot#3-download-file - * - */ @Service public class StorageService { - private static Logger log = LoggerFactory.getLogger(StorageService.class); - - @Autowired - private FileNameGenerator fileNameGenerator; + private static final Logger log = LoggerFactory.getLogger(StorageService.class); @Autowired private FileService fileService; @@ -48,7 +41,7 @@ public void store(MultipartFile file, int messageId) { String completeName = StringUtils.cleanPath(Objects.requireNonNull(file.getOriginalFilename())); String[] array = completeName.split("\\."); String fileExtension = array[array.length - 1]; - String fileName = fileNameGenerator.getRandomString(); + String fileName = UUID.randomUUID().toString(); String newName = fileName + "." + fileExtension; String uri = ServletUriComponentsBuilder.fromCurrentContextPath() diff --git a/backend/src/main/java/com/mercure/service/UserSeenMessageService.java b/backend/src/main/java/com/mercure/service/UserSeenMessageService.java index 2d93f5f..943406f 100644 --- a/backend/src/main/java/com/mercure/service/UserSeenMessageService.java +++ b/backend/src/main/java/com/mercure/service/UserSeenMessageService.java @@ -1,16 +1,12 @@ package com.mercure.service; import com.mercure.entity.*; -import com.mercure.repository.MessageRepository; import com.mercure.repository.UserSeenMessageRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.List; import java.util.Optional; -import java.util.Set; @Service public class UserSeenMessageService { @@ -18,9 +14,6 @@ public class UserSeenMessageService { @Autowired private UserSeenMessageRepository seenMessageRepository; - @Autowired - private UserService userService; - @Autowired private GroupService groupService; diff --git a/backend/src/main/java/com/mercure/service/UserService.java b/backend/src/main/java/com/mercure/service/UserService.java index c8d2fcb..39242f0 100644 --- a/backend/src/main/java/com/mercure/service/UserService.java +++ b/backend/src/main/java/com/mercure/service/UserService.java @@ -8,11 +8,11 @@ import com.mercure.entity.UserEntity; import com.mercure.mapper.UserMapper; import com.mercure.repository.UserRepository; +import jakarta.transaction.Transactional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import javax.transaction.Transactional; import java.text.Normalizer; import java.util.*; diff --git a/backend/src/main/java/com/mercure/utils/FileNameGenerator.java b/backend/src/main/java/com/mercure/utils/FileNameGenerator.java deleted file mode 100644 index dfd85af..0000000 --- a/backend/src/main/java/com/mercure/utils/FileNameGenerator.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.mercure.utils; - -import lombok.NoArgsConstructor; -import org.springframework.stereotype.Service; - -import java.security.SecureRandom; -import java.util.Locale; -import java.util.Random; - -@Service -@NoArgsConstructor -public class FileNameGenerator { - - /** - * Generate a random string. - */ - public String getRandomString() { - Random random = new SecureRandom(); - StringBuilder stringBuilder = new StringBuilder(); - for (int i = 0; i < StaticVariable.MAX_RANDOM_STRING_GEN; i++) - stringBuilder.append(alphanum.charAt(random.nextInt(alphanum.length()))); - return stringBuilder.toString(); - } - - public static final String upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - - public static final String lower = upper.toLowerCase(Locale.ROOT); - - public static final String digits = "0123456789"; - - public static final String alphanum = upper + lower + digits; - -} \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index fbbb687..03f92e6 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -3,26 +3,28 @@ server.port=9090 spring.main.allow-circular-references=true spring.liquibase.enabled=true -spring.liquibase.url=jdbc:mysql://localhost:3306/websocket +spring.liquibase.url=jdbc:mysql://localhost:3306/fastlitemessage_dev?createDatabaseIfNotExist=true spring.liquibase.change-log=classpath:/db/changelog-master.xml spring.liquibase.user=root spring.liquibase.password= logging.level.liquibase=info +spring.jpa.open-in-view=false +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect spring.batch.job.enabled=false spring.batch.jdbc.initialize-schema=always -spring.datasource.url=jdbc:mysql://localhost:3306/websocket +spring.datasource.url=jdbc:mysql://localhost:3306/fastlitemessage_dev spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.username=root spring.datasource.password= -spring.jpa.hibernate.use-new-id-generator-mappings=true -spring.jpa.open-in-view=false spring.servlet.multipart.max-file-size=5MB spring.servlet.multipart.max-request-size=5MB -logging.level.org.springframework.security = info -logging.level.org.springframework.web.cors.reactive.DefaultCorsProcessor=info +logging.level.web=debug +logging.level.org.springframework.security=debug +logging.level.org.springframework.web.cors.reactive.DefaultCorsProcessor=off +server.servlet.context-path=/api server.ssl.enabled=false \ No newline at end of file diff --git a/backend/src/main/resources/db/changelog/createFileBlobTable.xml b/backend/src/main/resources/db/changelog/createFileBlobTable.xml index ac011dc..feb1d1e 100644 --- a/backend/src/main/resources/db/changelog/createFileBlobTable.xml +++ b/backend/src/main/resources/db/changelog/createFileBlobTable.xml @@ -14,7 +14,7 @@ - + @@ -30,12 +30,6 @@ - - + @@ -25,12 +25,6 @@ - diff --git a/backend/src/main/resources/db/changelog/createMessageWsTable.xml b/backend/src/main/resources/db/changelog/createMessageWsTable.xml index 4ee6785..56c3356 100644 --- a/backend/src/main/resources/db/changelog/createMessageWsTable.xml +++ b/backend/src/main/resources/db/changelog/createMessageWsTable.xml @@ -14,7 +14,7 @@ - + @@ -33,12 +33,6 @@ - - + @@ -43,12 +43,6 @@ - diff --git a/frontend-web/.eslintrc.js b/frontend-web/.eslintrc.js new file mode 100644 index 0000000..5d6cf72 --- /dev/null +++ b/frontend-web/.eslintrc.js @@ -0,0 +1,39 @@ +module.exports = { + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended" + ], + "ignorePatterns": [ + ".js" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "react", + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "no-mixed-spaces-and-tabs": 0, + "linebreak-style": ["error", (process.platform === "win32" ? "windows" : "unix")], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "never" + ] + } +} diff --git a/frontend-web/.eslintrc.json b/frontend-web/.eslintrc.json deleted file mode 100644 index 40a84d4..0000000 --- a/frontend-web/.eslintrc.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "env": { - "browser": true, - "es2021": true - }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:@typescript-eslint/recommended" - ], - "ignorePatterns": [ - ".js" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": [ - "react", - "@typescript-eslint" - ], - "rules": { - "@typescript-eslint/no-explicit-any": "off", - "no-mixed-spaces-and-tabs": 0, - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "double" - ], - "semi": [ - "error", - "never" - ] - } -} diff --git a/frontend-web/.npmrc b/frontend-web/.npmrc new file mode 100644 index 0000000..b0b6fd6 --- /dev/null +++ b/frontend-web/.npmrc @@ -0,0 +1 @@ +8.19.2 diff --git a/frontend-web/.nvmrc b/frontend-web/.nvmrc new file mode 100644 index 0000000..bc78e9f --- /dev/null +++ b/frontend-web/.nvmrc @@ -0,0 +1 @@ +20.12.1 diff --git a/frontend-web/package.json b/frontend-web/package.json index 14c26c9..0aa6840 100644 --- a/frontend-web/package.json +++ b/frontend-web/package.json @@ -2,6 +2,10 @@ "name": "frontend-web", "version": "0.1.0", "private": true, + "author": { + "name": "Thibaut MOUTON", + "email": "thibautmouton22@gmail.com" + }, "scripts": { "start": "react-scripts start", "start:test": "tsc && set PORT=8080 && react-scripts start", diff --git a/frontend-web/public/assets/icons/Sound-Amplitude-Frame.svg b/frontend-web/public/assets/icons/Sound-Amplitude-Frame.svg deleted file mode 100644 index 8866eb5..0000000 --- a/frontend-web/public/assets/icons/Sound-Amplitude-Frame.svg +++ /dev/null @@ -1,816 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend-web/public/assets/icons/landing_logo.svg b/frontend-web/public/assets/icons/landing_logo.svg new file mode 100644 index 0000000..7df4fc6 --- /dev/null +++ b/frontend-web/public/assets/icons/landing_logo.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend-web/public/logo192.png b/frontend-web/public/logo192.png deleted file mode 100644 index fc44b0a3796c0e0a64c3d858ca038bd4570465d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5347 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN diff --git a/frontend-web/src/App.css b/frontend-web/src/App.css deleted file mode 100644 index f5fa847..0000000 --- a/frontend-web/src/App.css +++ /dev/null @@ -1,96 +0,0 @@ -#root { - height: 100%; -} - -::-webkit-scrollbar { - width: 10px; -} - -::-webkit-scrollbar-track { - background: #656565; -} - -/* Handle */ -::-webkit-scrollbar-thumb { - background: #c4c4c4; -} - -/* Handle on hover */ -::-webkit-scrollbar-thumb:hover { - background: #525252; -} - -.msg { - white-space: pre-wrap; - padding: 2px 0 2px 5px; -} - -.bold-unread-message-light { - font-weight: bold !important; - color: white !important; -} - -.bold-unread-message-dark { - font-weight: bold !important; - color: black !important; -} - -.hover-msg-light:hover { - background-color: cornflowerblue; -} - -.hover-msg-dark:hover { - background-color: #262626; -} - -.selected-group-light { - background-color: cornflowerblue !important; -} - -.selected-group-dark { - background-color: #262626 !important; -} - -.group-subtitle-color { - color: #787878 -} - -/************OVERRIDE MATERIAL UI STYLES******************************************************************************/ - -.MuiButton-root { - color: inherit !important; -} - -.MuiFormControl-marginNormal { - margin: 0 !important; -} - -/************ OVERRIDE ******************************************************************************/ -.lnk { - text-decoration: none; -} - -.mnu { - display: flex; -} - -.clrcstm { - color: inherit; - font-weight: inherit; -} - -/************ DARK & LIGHT MODE ******************************************************************************/ -.dark { - color: white; - background-color: #393939; -} - -.light { - color: black; - background-color: #ffffff; - /*background-color: #A9A9A9*/ -} - -.jsLink { - color: white; -} diff --git a/frontend-web/src/App.tsx b/frontend-web/src/App.tsx deleted file mode 100644 index 6d60763..0000000 --- a/frontend-web/src/App.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react" -import "./App.css" -import { LinearProgress } from "@mui/material" -import {BrowserRouter as Router} from "react-router-dom" -import { HeaderComponent } from "./components/partials/header-component" -import { useLoaderContext } from "./context/loader-context" -import { AlertComponent } from "./components/partials/alert-component" - -export const App = () => { - const { loading } = useLoaderContext() - - return ( - - { - loading && - - } - - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - {/**/} - - - ) -} diff --git a/frontend-web/src/components/create-group/create-group-component.tsx b/frontend-web/src/components/create-group/create-group-component.tsx index 0f285fe..9beb238 100644 --- a/frontend-web/src/components/create-group/create-group-component.tsx +++ b/frontend-web/src/components/create-group/create-group-component.tsx @@ -1,95 +1,111 @@ -import { Button, Container, CssBaseline, Grid, Typography } from "@mui/material" -import React, { useEffect, useState } from "react" -import { useThemeContext } from "../../context/theme-context" -import { CustomTextField } from "../partials/custom-material-textfield" -import { HttpService } from "../../service/http-service" +import {Button, Container, CssBaseline, Grid, Typography} from "@mui/material" +import React, {useContext, useEffect, useState} from "react" +import {useThemeContext} from "../../context/theme-context" +import {CustomTextField} from "../partials/custom-material-textfield" +import {HttpService} from "../../service/http-service" +import {AlertAction, AlertContext} from "../../context/AlertContext" export const CreateGroupComponent = () => { - const [groupName, setGroupName] = useState("") - const { theme } = useThemeContext() - const httpService = new HttpService() + const [groupName, setGroupName] = useState("") + const {theme} = useThemeContext() + const httpService = new HttpService() + const {dispatch} = useContext(AlertContext)! - useEffect(() => { - document.title = "Create group | FLM" - }, []) + useEffect(() => { + document.title = "Create group | FLM" + }, []) - function handleChange (event: any) { - event.preventDefault() - setGroupName(event.target.value) - } + function handleChange(event: any) { + event.preventDefault() + setGroupName(event.target.value) + } - async function createGroupByName (event: any) { - event.preventDefault() - if (groupName !== "") { - const res = await httpService.createGroup(groupName) - // dispatch(setAlerts({ - // alert: { - // isOpen: true, - // alert: "success", - // text: `Group "${groupName}" has been created successfully` - // } - // })) - // dispatch(createGroup({ group: res.data })) - // history.push({ - // pathname: "/t/messages/" + res.data.url - // }) - // setAlerts([...alerts, new FeedbackModel(UUIDv4(), `Cannot create group "${groupName}" : ${err.toString()}`, "error", true)]) + async function createGroupByName(event: any) { + event.preventDefault() + if (groupName !== "") { + try { + await httpService.createGroup(groupName) + dispatch({ + type: AlertAction.ADD_ALERT, + payload: { + id: crypto.randomUUID(), + isOpen: true, + alert: "success", + text: `Group "${groupName}" has been created successfully` + } + }) + } catch (error) { + dispatch({ + type: AlertAction.ADD_ALERT, + payload: { + id: crypto.randomUUID(), + isOpen: true, + alert: "error", + text: `Cannot create group "${groupName}" : ${error}` + } + }) + } + // dispatch(createGroup({ group: res.data })) + // history.push({ + // pathname: "/t/messages/" + res.data.url + // }) + // setAlerts([...alerts, new FeedbackModel(UUIDv4(), `Cannot create group "${groupName}" : ${err.toString()}`, "error", true)]) + } } - } - function submitGroupCreation (event: any) { - if (event.key === undefined || event.key === "Enter") { - if (groupName === "") { - return - } - createGroupByName(event) + function submitGroupCreation(event: any) { + if (event.key === undefined || event.key === "Enter") { + if (groupName === "") { + return + } + createGroupByName(event) + } } - } - return ( -
- - -
- - Create a group - -
-
- - - - -
- - - -
-
-
-
-
- ) + return ( +
+ + +
+ + Create a group + +
+
+ + + + +
+ + + +
+
+
+
+
+ ) } diff --git a/frontend-web/src/components/home.tsx b/frontend-web/src/components/home.tsx index 8831d9d..21849b9 100644 --- a/frontend-web/src/components/home.tsx +++ b/frontend-web/src/components/home.tsx @@ -1,49 +1,50 @@ import {Box, Card, CardContent, Grid, Typography} from "@mui/material" -import React, {useEffect} from "react" -import { Link as RouterLink } from "react-router-dom" -import { generateColorMode } from "./utils/enable-dark-mode" -import { useAuthContext } from "../context/auth-context" -import { useLoaderContext } from "../context/loader-context" -import { useThemeContext } from "../context/theme-context" +import React, {useContext, useEffect} from "react" +import {generateColorMode} from "./utils/enable-dark-mode" +import {useThemeContext} from "../context/theme-context" +import {LoginComponent} from "./login/login-component" +import {FooterComponent} from "./partials/footer-component" +import {AuthUserContext} from "../context/AuthContext" export const HomeComponent = (): React.JSX.Element => { - const { theme } = useThemeContext() - const { user } = useAuthContext() - const { setLoading } = useLoaderContext() + const {theme} = useThemeContext() + const {user} = useContext(AuthUserContext)! - useEffect(() => { - document.title = "Home | FLM" - // setLoading(false) - }, [setLoading]) + useEffect(() => { + document.title = "Home | FLM" + }, []) - return ( -
- - - -

Welcome {user ? "back " + user.firstName : "visitor"} !

- - - - Word of the Day - - - - { - user - ?

You have 0 unread messages

- :
To start using FastLiteMessage, please register here -
- } -
-
-
-
- ) + return ( +
+ + + + + + + Welcome to FastLiteMessage {user?.firstName} + + + {"test + + + Simple, fast and secure + +
FastLiteMessage allow to communicate with other people, create groups, make serverless video calls in an easy way. Log into your account or register to start using FastLiteMessage.
+ +
+
+
+ + + +
+
+
+ ) } diff --git a/frontend-web/src/components/login/login-component.tsx b/frontend-web/src/components/login/login-component.tsx index b7b3236..9c1226e 100644 --- a/frontend-web/src/components/login/login-component.tsx +++ b/frontend-web/src/components/login/login-component.tsx @@ -1,152 +1,147 @@ import LockIcon from "@mui/icons-material/Lock" -import { Button, Grid, Typography } from "@mui/material" -import React, { useEffect, useState } from "react" -import { Link } from "react-router-dom" -import { useLoaderContext } from "../../context/loader-context" -import { useThemeContext } from "../../context/theme-context" -import { generateIconColorMode, generateLinkColorMode } from "../utils/enable-dark-mode" -import { FooterComponent } from "../partials/footer-component" -import { CustomTextField } from "../partials/custom-material-textfield" -import { HttpService } from "../../service/http-service" +import {Button, Grid, Typography} from "@mui/material" +import React, {useContext, useEffect, useState} from "react" +import {Link, redirect} from "react-router-dom" +import {useThemeContext} from "../../context/theme-context" +import {generateIconColorMode, generateLinkColorMode} from "../utils/enable-dark-mode" +import {CustomTextField} from "../partials/custom-material-textfield" +import {HttpService} from "../../service/http-service" +import {LoaderContext} from "../../context/loader-context" +import {AlertAction, AlertContext} from "../../context/AlertContext" export const LoginComponent: React.FunctionComponent = () => { - const [username, setUsername] = useState("") - const [password, setPassword] = useState("") - // const dispatch = useDispatch() - const { theme } = useThemeContext() - const { setLoading } = useLoaderContext() - const httpService = new HttpService() + const [username, setUsername] = useState("") + const [password, setPassword] = useState("") - useEffect(() => { - document.title = "Login | FLM" - }, []) + const {dispatch} = useContext(AlertContext)! + const {setLoading} = useContext(LoaderContext) + const {theme} = useThemeContext() + const httpService = new HttpService() - function handleChange (e: any) { - e.preventDefault() - switch (e.target.name) { - case "username": - setUsername(e.target.value) - break - case "password": - setPassword(e.target.value) - break - default: - throw Error("Whoops ! Something went wrong...") + useEffect(() => { + document.title = "Login | FLM" + }, []) + + function handleChange(e: any) { + e.preventDefault() + switch (e.target.name) { + case "username": + setUsername(e.target.value) + break + case "password": + setPassword(e.target.value) + break + default: + throw Error("Whoops ! Something went wrong...") + } } - } - function submitLogin (event: any) { - if (event.key === undefined || event.key === "Enter") { - if (!username || !password) { - return - } - login() + function submitLogin(event: any) { + if (event.key === undefined || event.key === "Enter") { + if (!username || !password) { + return + } + login() + } } - } - const login = async () => { - setLoading(true) - try { - await httpService.authenticate({ - username, - password - }) - // dispatch(setAlerts({ - // alert: { - // text: "You are connected", - // alert: "info", - // isOpen: true - // } - // })) - // history.push("/") - } catch (err: any) { - // dispatch(setAlerts({ - // alert: { - // text: err.message, - // alert: "error", - // isOpen: true - // } - // })) - setUsername("") - setPassword("") - } finally { - // setLoading(false) + const login = async () => { + setLoading(true) + try { + await httpService.authenticate({ + username, + password + }) + redirect("t/messages") + } catch (err: any) { + dispatch({ + type: AlertAction.ADD_ALERT, + payload: { + id: crypto.randomUUID(), + text: err.message, + alert: "error", + isOpen: true + } + }) + setUsername("") + setPassword("") + } finally { + setLoading(false) + } } - } - return ( -
-
-
- -
- - Sign in - -
- - - - - - - - -
- - - -
- - - Forgot your password ? - - - Sign up - - -
- -
-
- ) + return ( +
+
+
+ +
+ + Sign in + +
+ + + + + + + + +
+ + + +
+ + + Forgot your password ? + + + Sign up + + +
+
+
+ ) } diff --git a/frontend-web/src/components/message/CreateMessageComponent.tsx b/frontend-web/src/components/message/CreateMessageComponent.tsx new file mode 100644 index 0000000..210c4a5 --- /dev/null +++ b/frontend-web/src/components/message/CreateMessageComponent.tsx @@ -0,0 +1,165 @@ +import {IconButton} from "@mui/material" +import {CustomTextField} from "../partials/custom-material-textfield" +import React, {useContext, useState} from "react" +import {getPayloadSize} from "../../utils/string-size-calculator" +import {TransportModel} from "../../interface-contract/transport-model" +import {TransportActionEnum} from "../../utils/transport-action-enum" +import {HttpService} from "../../service/http-service" +import HighlightOffIcon from "@mui/icons-material/HighlightOff" +import {WebSocketContext} from "../../context/WebsocketContext" +import {AuthUserContext} from "../../context/AuthContext" + +interface CreateMessageComponentProps { + groupUrl: string +} + +export function CreateMessageComponent({groupUrl}: CreateMessageComponentProps): React.JSX.Element { + const {ws} = useContext(WebSocketContext)! + const {user} = useContext(AuthUserContext)! + const [, setImageLoaded] = useState(false) + const [message, setMessage] = useState("") + const [file, setFile] = React.useState(null) + + const [imagePreviewUrl, setImagePreviewUrl] = React.useState("") + + function submitMessage(event: any) { + if (message !== "") { + if (event.key !== undefined && event.shiftKey && event.keyCode === 13) { + return + } + if (event.key !== undefined && event.keyCode === 13) { + event.preventDefault() + sendMessage() + setMessage("") + } + } + } + + function handleChange(event: any) { + setMessage(event.target.value) + } + + function previewFile(event: any) { + resetImageBuffer(event) + const reader = new FileReader() + const file = event.target.files[0] + reader.readAsDataURL(file) + + reader.onload = (e) => { + if (e.target && e.target.readyState === FileReader.DONE) { + setFile(file) + setImagePreviewUrl(reader.result as string) + setImageLoaded(true) + } + } + } + + function resetImageBuffer(event: any) { + event.preventDefault() + setFile(null) + setImagePreviewUrl("") + setImageLoaded(false) + } + + async function sendMessage() { + if (message !== "") { + if (getPayloadSize(message) < 8192 && ws?.active) { + const transport = new TransportModel(user?.id || 0, TransportActionEnum.SEND_GROUP_MESSAGE, undefined, groupUrl, message) + console.log("SENDING MESSAGE", transport) + ws.publish({ + destination: "/message", + body: JSON.stringify(transport) + }) + } + setMessage("") + } + if (file !== null) { + const httpService = new HttpService() + const formData = new FormData() + formData.append("file", file) + formData.append("userId", String(user?.id || 0)) + formData.append("groupUrl", groupUrl || "") + await httpService.uploadFile(formData) + setMessage("") + setImageLoaded(false) + setFile(null) + setImagePreviewUrl("") + } + } + + function markMessageSeen() { + // dispatch(markMessageAsSeen({ + // groupUrl + // })) + } + + + return ( + <> +
+ { + imagePreviewUrl && +
+ resetImageBuffer(event)}> + + +
+ } +
+
+ previewFile(event)} + /> + handleChange(event)} + type={"text"} + keyUp={submitMessage} + isMultiline={true} + isDarkModeEnable={"true"} + name={"mainWriteMessage"}/> + {/**/} + {/* */} + {/**/} +
+ + ) +} diff --git a/frontend-web/src/components/partials/HeaderComponent.tsx b/frontend-web/src/components/partials/HeaderComponent.tsx new file mode 100644 index 0000000..9f2b783 --- /dev/null +++ b/frontend-web/src/components/partials/HeaderComponent.tsx @@ -0,0 +1,132 @@ +import ClearAllIcon from "@mui/icons-material/ClearAll" +import {Button, FormControlLabel, Skeleton, Switch, Toolbar, Typography} from "@mui/material" +import React, {useContext} from "react" +import {HttpService} from "../../service/http-service" +import {AuthUserContext} from "../../context/AuthContext" + +export function HeaderComponent(): React.JSX.Element { + const {user, setUser} = useContext(AuthUserContext)! + const { + authLoading, + } = {authLoading: true} + + const httpService = new HttpService() + + const theme = "light" + // useEffect(() => { + // setCookie("pref-theme", theme) + // }, [theme]) + + function toggleThemeMode() { + // toggleTheme() + } + + async function logoutUser(event: React.MouseEvent) { + event.preventDefault() + await httpService.logout() + setUser(undefined) + // dispatch(setAlerts({ + // alert: { + // text: "You log out successfully", + // alert: "success", + // isOpen: true + // } + // })) + // history.push("/") + } + + function generateLoading() { + return [1, 2, 3, 4].map((index) => ( +
+ +
+ )) + } + + return ( + <> +
+ + + {/**/} + FastLiteMessage + {/**/} + + + +
+ + ) +} diff --git a/frontend-web/src/components/partials/no-data-component.tsx b/frontend-web/src/components/partials/NoDataComponent.tsx similarity index 89% rename from frontend-web/src/components/partials/no-data-component.tsx rename to frontend-web/src/components/partials/NoDataComponent.tsx index d028cea..56cb1b7 100644 --- a/frontend-web/src/components/partials/no-data-component.tsx +++ b/frontend-web/src/components/partials/NoDataComponent.tsx @@ -2,7 +2,7 @@ import React from "react" import CommentsDisabledIcon from "@mui/icons-material/CommentsDisabled" import { Box } from "@mui/material" -export const NoDataComponent = (): JSX.Element => { +export function NoDataComponent(): React.JSX.Element { return ( diff --git a/frontend-web/src/components/partials/alert-component.tsx b/frontend-web/src/components/partials/alert-component.tsx index 0715d6c..8144837 100644 --- a/frontend-web/src/components/partials/alert-component.tsx +++ b/frontend-web/src/components/partials/alert-component.tsx @@ -1,40 +1,34 @@ -import { Alert, Collapse } from "@mui/material" -import React from "react" +import {Alert, AlertTitle, Collapse} from "@mui/material" +import React, {useContext} from "react" +import {AlertAction, AlertContext, AlertContextProvider} from "../../context/AlertContext" export const AlertComponent: React.FunctionComponent = () => { - const alerts: any[] = [] + const {alerts, dispatch} = useContext(AlertContext)! - function closeAlert (id?: string) { - if (!id) { - return + function closeAlert(id: string) { + dispatch({type: AlertAction.DELETE_ALERT, payload: id}) } - const indexToDelete = alerts.findIndex((elt) => elt.id === id) - const allAlerts = [...alerts] - const eltToDelete = { ...allAlerts[indexToDelete] } - eltToDelete.isOpen = false - allAlerts[indexToDelete] = eltToDelete - // dispatch(setAllAlerts({ allAlerts })) - } - return ( -
- { - alerts.map((value) => ( -
- - closeAlert(value.id)} - severity={value.alert} - variant={"standard"}> - {value.text} - - -
- )) - } -
- ) + return ( +
+ { + alerts.map((value) => ( +
+ + closeAlert(value.id)} + severity={value.alert} + variant={"standard"}> + Success + {value.text} + + +
+ )) + } +
+ ) } diff --git a/frontend-web/src/components/partials/all-users-dialog.tsx b/frontend-web/src/components/partials/all-users-dialog.tsx index 6b9844e..534b193 100644 --- a/frontend-web/src/components/partials/all-users-dialog.tsx +++ b/frontend-web/src/components/partials/all-users-dialog.tsx @@ -1,7 +1,6 @@ import AccountCircleIcon from "@mui/icons-material/AccountCircle" import { Avatar, Dialog, DialogTitle, List, ListItemAvatar, ListItemButton, ListItemText } from "@mui/material" import React from "react" -import { useAuthContext } from "../../context/auth-context" import { useThemeContext } from "../../context/theme-context" import { GroupUserModel } from "../../interface-contract/group-user-model" import { generateIconColorMode } from "../utils/enable-dark-mode" @@ -22,7 +21,7 @@ export const AllUsersDialog: React.FunctionComponent = ({ action }) => { const { theme } = useThemeContext() - const { user } = useAuthContext() + const { user } = {} as any // TODO remove any return ( prop !== "color" && prop !== "myProp", -})<{ myProp: string }>(({ - myProp, -}) => ({ - "& label.Mui-focused": { - color: myProp === "dark" ? "white" : "black", - }, - "& .MuiInputLabel-formControl": { - color: myProp === "dark" ? "white" : "black", - }, - "& .MuiInput-underline": { - color: myProp === "dark" ? "white" : "black", - }, - "& .MuiOutlinedInput-input": { - color: myProp === "dark" ? "white" : "black", - }, - "& .MuiOutlinedInput-root": { - "& fieldset": { - borderColor: myProp === "dark" ? "white" : "black", - }, - "&:hover fieldset": { - borderColor: myProp === "dark" ? "white" : "black", - }, - "&.Mui-focused fieldset": { - borderColor: myProp === "dark" ? "white" : "black", - }, - }, -})) +// const StyledComp = styled(TextField, { +// shouldForwardProp: (prop) => prop !== "color" && prop !== "myProp", +// })<{ myProp: string }>(({ +// myProp, +// }) => ({ +// "& label.Mui-focused": { +// color: myProp === "dark" ? "white" : "black", +// }, +// "& .MuiInputLabel-formControl": { +// color: myProp === "dark" ? "white" : "black", +// }, +// "& .MuiInput-underline": { +// color: myProp === "dark" ? "white" : "black", +// }, +// "& .MuiOutlinedInput-input": { +// color: myProp === "dark" ? "white" : "black", +// }, +// "& .MuiOutlinedInput-root": { +// "& fieldset": { +// borderColor: myProp === "dark" ? "white" : "black", +// }, +// "&:hover fieldset": { +// borderColor: myProp === "dark" ? "white" : "black", +// }, +// "&.Mui-focused fieldset": { +// borderColor: myProp === "dark" ? "white" : "black", +// }, +// }, +// })) interface ICustomMaterialTextField { - id: string | undefined - label: string - value: string - name: string - isMultiline: boolean - type: "password" | "text" - handleChange: (event: any) => void - isDarkModeEnable: string - onClick?: () => void - keyUp?: (event: any) => void - keyDown?: (event: any) => void + id: string | undefined + label: string + value: string + name: string + isMultiline: boolean + type: "password" | "text" + handleChange: (event: any) => void + isDarkModeEnable: string + onClick?: () => void + keyUp?: (event: any) => void + keyDown?: (event: any) => void } export const CustomTextField: FunctionComponent = (props) => { - const { theme } = useThemeContext() + // const { theme } = useThemeContext() - const handleChange = (event: any) => { - props.handleChange(event) - } - - const submitForm = (event: any) => { - if (props.keyUp !== undefined) { - props.keyUp(event) + const handleChange = (event: any) => { + props.handleChange(event) } - if (props.keyDown !== undefined) { - props.keyDown(event) + + const submitForm = (event: any) => { + if (props.keyUp !== undefined) { + props.keyUp(event) + } + if (props.keyDown !== undefined) { + props.keyDown(event) + } } - } - return ( - - submitForm(event)} - onKeyDown={(event) => submitForm(event)} - /> - - ) + return ( + + submitForm(event)} + onKeyDown={(event) => submitForm(event)} + // startAdornment={ + // + // + // + // + // + // + // + // } + // endAdornment={ + // + // + // + // + // + // } + /> + + ) } diff --git a/frontend-web/src/components/partials/footer-component.tsx b/frontend-web/src/components/partials/footer-component.tsx index 388313c..2fd6c1b 100644 --- a/frontend-web/src/components/partials/footer-component.tsx +++ b/frontend-web/src/components/partials/footer-component.tsx @@ -13,7 +13,7 @@ export const FooterComponent = () => { href={"https://github.com/Thibaut-Mouton/react-spring-messenger-project"} rel="noreferrer"> FastLiteMessage - Open source software - {" - "} + {" | "} {new Date().getFullYear()}
diff --git a/frontend-web/src/components/partials/header-component.tsx b/frontend-web/src/components/partials/header-component.tsx deleted file mode 100644 index a09c46e..0000000 --- a/frontend-web/src/components/partials/header-component.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import ClearAllIcon from "@mui/icons-material/ClearAll" -import {Button, FormControlLabel, Skeleton, Switch, Toolbar, Typography} from "@mui/material" -import React, {useEffect, useState} from "react" -import {useCookies} from "react-cookie" -import {Link as RouterLink} from "react-router-dom" -import {useAuthContext} from "../../context/auth-context" -import {useThemeContext} from "../../context/theme-context" -import {HttpService} from "../../service/http-service" -import {useLocation} from "react-router-dom" - -export const HeaderComponent: React.FunctionComponent = () => { - const { - user, - setUser - } = useAuthContext() - const { - authLoading, - currentActiveGroup, - } = {authLoading: true, currentActiveGroup: ""} - const { - theme, - toggleTheme - } = useThemeContext() - const httpService = new HttpService() - const [cookie, setCookie] = useCookies() - const [isHeaderCouldRender, setHeaderRender] = useState(false) - const location = useLocation() - - useEffect(() => { - const isCurrentPathVideoComponent = location.pathname.split("/")[1] !== "call" - if (isCurrentPathVideoComponent) { - setHeaderRender(true) - } else { - setHeaderRender(false) - } - }, [user]) - - useEffect(() => { - setCookie("pref-theme", theme) - }, [theme]) - - function toggleThemeMode() { - toggleTheme() - } - - async function logoutUser(event: React.MouseEvent) { - event.preventDefault() - await httpService.logout() - setUser(undefined) - // dispatch(setAlerts({ - // alert: { - // text: "You log out successfully", - // alert: "success", - // isOpen: true - // } - // })) - // history.push("/") - } - - function generateLoading() { - return [1, 2, 3, 4].map((index) => ( -
- -
- )) - } - - return ( - <> - { - isHeaderCouldRender && -
- - - - FastLiteMessage - - - - -
- } - - ) -} diff --git a/frontend-web/src/components/partials/loader/LoaderComponent.tsx b/frontend-web/src/components/partials/loader/LoaderComponent.tsx new file mode 100644 index 0000000..ce5d5dd --- /dev/null +++ b/frontend-web/src/components/partials/loader/LoaderComponent.tsx @@ -0,0 +1,16 @@ +import {LinearProgress} from "@mui/material" +import React, {useContext} from "react" +import {LoaderContext} from "../../../context/loader-context" + +export function LoaderComponent() { + const {loading} = useContext(LoaderContext) + return <> + { + loading && + } + +} diff --git a/frontend-web/src/components/partials/video/active-video-call.tsx b/frontend-web/src/components/partials/video/active-video-call.tsx index 7930ffe..5326757 100644 --- a/frontend-web/src/components/partials/video/active-video-call.tsx +++ b/frontend-web/src/components/partials/video/active-video-call.tsx @@ -1,37 +1,39 @@ import React from "react" -import { Box, Button, Icon } from "@mui/material" +import {Box, Button, Icon} from "@mui/material" -export const ActiveVideoCall: React.FunctionComponent<{ isAnyCallActive: boolean }> = ({ isAnyCallActive }): JSX.Element => { +export const ActiveVideoCall: React.FunctionComponent<{ + isAnyCallActive: boolean +}> = ({isAnyCallActive}): React.JSX.Element => { - return ( - <> - { - isAnyCallActive && - - - - } - - ) + fontSize: "14px", + paddingLeft: "8px" + }}>Call in progress + + + } + + ) } diff --git a/frontend-web/src/components/register/register-form.css b/frontend-web/src/components/register/register-form.css index 60bd8ae..6b222bc 100644 --- a/frontend-web/src/components/register/register-form.css +++ b/frontend-web/src/components/register/register-form.css @@ -5,8 +5,6 @@ display: flex; flex-direction: column; align-items: center; - margin-left: 32%; - margin-right: 32%; } @media (max-width: 1250px) { @@ -14,4 +12,4 @@ margin-left: 20%; margin-right: 20%; } -} \ No newline at end of file +} diff --git a/frontend-web/src/components/register/register-user.tsx b/frontend-web/src/components/register/register-user.tsx index 1a3b8fa..9e087f6 100644 --- a/frontend-web/src/components/register/register-user.tsx +++ b/frontend-web/src/components/register/register-user.tsx @@ -3,17 +3,14 @@ import CloseIcon from "@mui/icons-material/Close" import { Alert, Button, Collapse, Grid, IconButton, Typography } from "@mui/material" import React from "react" import { Link } from "react-router-dom" -import { useLoaderContext } from "../../context/loader-context" import { useThemeContext } from "../../context/theme-context" import { HttpService } from "../../service/http-service" import "./register-form.css" import { CustomTextField } from "../partials/custom-material-textfield" import { generateColorMode, generateIconColorMode, generateLinkColorMode } from "../utils/enable-dark-mode" -import { FooterComponent } from "../partials/footer-component" export const RegisterFormComponent = (): JSX.Element => { const { theme } = useThemeContext() - const { setLoading } = useLoaderContext() const [username, setUsername] = React.useState("") const [lastName, setLastName] = React.useState("") const [email, setEmail] = React.useState("") @@ -53,7 +50,6 @@ export const RegisterFormComponent = (): JSX.Element => { async function registerUser (event: React.MouseEvent) { event.preventDefault() - setLoading(true) errorArray.length = 0 const result = checkFormValidation() if (result.length === 0) { @@ -145,7 +141,7 @@ export const RegisterFormComponent = (): JSX.Element => { return (
@@ -289,7 +285,6 @@ export const RegisterFormComponent = (): JSX.Element => {
-
) } diff --git a/frontend-web/src/components/utils/enable-dark-mode.ts b/frontend-web/src/components/utils/enable-dark-mode.ts index c2dbba3..2c65dd9 100644 --- a/frontend-web/src/components/utils/enable-dark-mode.ts +++ b/frontend-web/src/components/utils/enable-dark-mode.ts @@ -3,7 +3,7 @@ export const generateColorMode = (isDarkMode: string): string => { } export const generateIconColorMode = (isDarkMode: string): string => { - return isDarkMode === "dark" ? "#dcdcdc" : "#4A4A4A" + return isDarkMode !== "dark" ? "#dcdcdc" : "#4A4A4A" } export const generateLinkColorMode = (isDarkMode: string): string => { diff --git a/frontend-web/src/components/websocket/call-window-component.tsx b/frontend-web/src/components/websocket/CallWindowComponent.tsx similarity index 78% rename from frontend-web/src/components/websocket/call-window-component.tsx rename to frontend-web/src/components/websocket/CallWindowComponent.tsx index 62a585f..16df0c2 100644 --- a/frontend-web/src/components/websocket/call-window-component.tsx +++ b/frontend-web/src/components/websocket/CallWindowComponent.tsx @@ -1,15 +1,17 @@ import CallIcon from "@mui/icons-material/Call" import {Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material" -import React from "react" -import {useWebSocketContext} from "../../context/ws-context" +import React, {useContext} from "react" import {RtcTransportDTO} from "../../interface-contract/rtc-transport-model" import {RtcActionEnum} from "../../utils/rtc-action-enum" +import {WebSocketContext} from "../../context/WebsocketContext" -export const CallWindowComponent: React.FunctionComponent<{ userId: number, groupUrl: string }> = ({ - userId, - groupUrl - }) => { - const {ws} = useWebSocketContext() +interface CallWindowComponentProps { + userId: number + groupUrl?: string +} + +export function CallWindowComponent({userId, groupUrl}: CallWindowComponentProps) { + const {ws} = useContext(WebSocketContext)! const { callStarted, callUrl @@ -19,9 +21,9 @@ export const CallWindowComponent: React.FunctionComponent<{ userId: number, grou event.preventDefault() const startedCallUrl = crypto.randomUUID() if (ws) { - const transport = new RtcTransportDTO(userId, groupUrl, RtcActionEnum.INIT_ROOM) + const transport = new RtcTransportDTO(userId, groupUrl || "", RtcActionEnum.INIT_ROOM) ws.publish({ - destination: `/app/rtc/${startedCallUrl}`, + destination: `/rtc/${startedCallUrl}`, body: JSON.stringify(transport) }) const callPage = window.open(`http://localhost:3000/call/${startedCallUrl}`, "_blank") as any diff --git a/frontend-web/src/components/websocket/video-component-style.css b/frontend-web/src/components/websocket/video-component-style.css deleted file mode 100644 index 4e8b481..0000000 --- a/frontend-web/src/components/websocket/video-component-style.css +++ /dev/null @@ -1,3 +0,0 @@ -body, html { - background-color: #4A4A4A; -} diff --git a/frontend-web/src/components/websocket/video-component.tsx b/frontend-web/src/components/websocket/video-component.tsx index 01c36c9..324a964 100644 --- a/frontend-web/src/components/websocket/video-component.tsx +++ b/frontend-web/src/components/websocket/video-component.tsx @@ -1,242 +1,242 @@ -import React, { useEffect, useState } from "react" -import { initWebSocket } from "../../config/websocket-config" -import { Client, IMessage } from "@stomp/stompjs" -import { Box, Skeleton } from "@mui/material" -import { useLocation } from "react-router-dom" -import { RtcTransportDTO } from "../../interface-contract/rtc-transport-model" -import { RtcActionEnum } from "../../utils/rtc-action-enum" -import { SoundControl } from "../partials/video/sound-control" -import { VideoControl } from "../partials/video/video-control" -import { EmptyRoom } from "../partials/video/empty-room" -import "./video-component-style.css" -import { HangUpControl } from "../partials/video/hang-up-control" -import { CallEnded } from "../partials/video/call-ended" -import { HttpService } from "../../service/http-service" +import React, {useEffect, useState} from "react" +import {initWebSocket} from "../../config/websocket-config" +import {Client, IMessage} from "@stomp/stompjs" +import {Box, Skeleton} from "@mui/material" +import {useLocation} from "react-router-dom" +import {RtcTransportDTO} from "../../interface-contract/rtc-transport-model" +import {RtcActionEnum} from "../../utils/rtc-action-enum" +import {SoundControl} from "../partials/video/sound-control" +import {VideoControl} from "../partials/video/video-control" +import {EmptyRoom} from "../partials/video/empty-room" +import {HangUpControl} from "../partials/video/hang-up-control" +import {CallEnded} from "../partials/video/call-ended" +import {HttpService} from "../../service/http-service" const getUuid = (location: string): string => { - const temp = location.split("/") - return temp[temp.length - 1] + const temp = location.split("/") + return temp[temp.length - 1] } -export const VideoComponent = (): JSX.Element => { - const [ws, setWs] = useState() - const [currentUserId, setCurrentUserId] = useState(-1) - const [isPageAuthorized, setPageStatus] = useState(true) - const [callEnded, setCallEnded] = useState(false) - const [currentLocalStream, setLocalStream] = useState() - - // const configuration = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' }] } - const configuration = { "iceServers": [] } - const [localVideoReady, setLocalVideoState] = useState(false) - const peerConnection = new RTCPeerConnection(configuration) - const location = useLocation() - - const isUserInitiateSession = location.search.split("=")[1] - const roomUrl = getUuid(location.pathname) - const groupUrlFromParent = (window as any).groupUrl - const http = new HttpService() - - peerConnection.addEventListener("connectionstatechange", () => { - switch (peerConnection.connectionState) { - case "new": - console.log("Connecting...") - break - case "connected": - console.log("Online") - break - case "disconnected": - console.log("Disconnecting...") - break - case "closed": - console.log("Offline") - break - case "failed": - console.log("Error") - break - default: - console.log("Unknown") - break - } - }) - - peerConnection.addEventListener("icecandidate", (event) => { - console.log("EVENT", event.candidate) - if (ws && event.candidate) { - const iceCandidateResponse = new RtcTransportDTO(currentUserId, "", RtcActionEnum.ICE_CANDIDATE, undefined, undefined, event.candidate) - ws.publish({ - destination: `/app/rtc/${roomUrl}`, - body: JSON.stringify(iceCandidateResponse) - }) - } - }) - - peerConnection.addEventListener("icecandidateerror", (event) => { - // eslint-disable-next-line no-console - console.log("ERROR EVENT", event) - }) - - // peerConnection.addEventListener('icegatheringstatechange', (event) => { - // console.log('icegatheringstatechange', event) - // }) - - peerConnection.addEventListener("track", (event) => { - const remoteVideo = document.querySelector("video#localVideo") as HTMLVideoElement - const [remoteStream] = event.streams - remoteVideo.srcObject = remoteStream - }) - - useEffect(() => { - initRTC().then((res) => { - if (res) { - initWs() - } +export const VideoComponent = (): React.JSX.Element => { + const [ws, setWs] = useState() + const [currentUserId, setCurrentUserId] = useState(-1) + const [isPageAuthorized, setPageStatus] = useState(true) + const [callEnded, setCallEnded] = useState(false) + const [currentLocalStream, setLocalStream] = useState() + + // const configuration = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' }] } + const configuration = {"iceServers": []} + const [localVideoReady, setLocalVideoState] = useState(false) + const peerConnection = new RTCPeerConnection(configuration) + const location = useLocation() + + const isUserInitiateSession = location.search.split("=")[1] + const roomUrl = getUuid(location.pathname) + const groupUrlFromParent = (window as any).groupUrl + const http = new HttpService() + + peerConnection.addEventListener("connectionstatechange", () => { + switch (peerConnection.connectionState) { + case "new": + console.log("Connecting...") + break + case "connected": + console.log("Online") + break + case "disconnected": + console.log("Disconnecting...") + break + case "closed": + console.log("Offline") + break + case "failed": + console.log("Error") + break + default: + console.log("Unknown") + break + } }) - }, [groupUrlFromParent]) - - const initWs = async () => { - const { data } = await http.pingRoute() - const { user } = data - setCurrentUserId(user.id) - const wsObj = initWebSocket(user.wsToken) - - setWs(wsObj) - wsObj.onConnect = async () => { - wsObj.subscribe(`/topic/rtc/${user.id}`, async (res: IMessage) => { - const rtcTransportDto = JSON.parse(res.body) as RtcTransportDTO - switch (rtcTransportDto.action) { - case RtcActionEnum.SEND_ANSWER: { - if (rtcTransportDto.answer) { - await peerConnection.setRemoteDescription(new RTCSessionDescription(rtcTransportDto.answer)) - } - break - } - case RtcActionEnum.SEND_OFFER: { - if (rtcTransportDto.offer) { - await peerConnection.setRemoteDescription(new RTCSessionDescription(rtcTransportDto.offer)) - const answer = await peerConnection.createAnswer() - await peerConnection.setLocalDescription(answer) - const answerTransport = new RtcTransportDTO(user.id, "", RtcActionEnum.SEND_ANSWER, undefined, answer) - wsObj.publish({ - destination: `/app/rtc/${roomUrl}`, - body: JSON.stringify(answerTransport) - }) - } - break - } - case RtcActionEnum.ICE_CANDIDATE: { - if (rtcTransportDto.iceCandidate) { - await peerConnection.addIceCandidate(rtcTransportDto.iceCandidate) - } - break - } - default: - break - } - }) - if (isUserInitiateSession === "join") { - const offer = await peerConnection.createOffer() - await peerConnection.setLocalDescription(offer) - const transport = new RtcTransportDTO(user.id, "", RtcActionEnum.JOIN_ROOM, offer) - wsObj.publish({ - destination: `/app/rtc/${roomUrl}`, - body: JSON.stringify(transport) - }) - } - } - wsObj.activate() - } - - const changeVideoStatus = (stopVideo: boolean) => { - if (currentLocalStream) { - if (stopVideo) { - currentLocalStream.getTracks().forEach((track) => { - track.stop() - }) - } else { - currentLocalStream.getTracks().forEach((track) => { - peerConnection.addTrack(track, currentLocalStream) - }) - } - } - } - const initRTC = async (): Promise => { - const url = getUuid(location.pathname) + peerConnection.addEventListener("icecandidate", (event) => { + console.log("EVENT", event.candidate) + if (ws && event.candidate) { + const iceCandidateResponse = new RtcTransportDTO(currentUserId, "", RtcActionEnum.ICE_CANDIDATE, undefined, undefined, event.candidate) + ws.publish({ + destination: `/rtc/${roomUrl}`, + body: JSON.stringify(iceCandidateResponse) + }) + } + }) + + peerConnection.addEventListener("icecandidateerror", (event) => { + // eslint-disable-next-line no-console + console.log("ERROR EVENT", event) + }) + + // peerConnection.addEventListener('icegatheringstatechange', (event) => { + // console.log('icegatheringstatechange', event) + // }) + + peerConnection.addEventListener("track", (event) => { + const remoteVideo = document.querySelector("video#localVideo") as HTMLVideoElement + const [remoteStream] = event.streams + remoteVideo.srcObject = remoteStream + }) - const urlCheckResponse = await http.ensureRoomExists(url) - setPageStatus(urlCheckResponse.data) - if (urlCheckResponse && !urlCheckResponse.data) { - return false + useEffect(() => { + initRTC().then((res) => { + if (res) { + initWs() + } + }) + }, [groupUrlFromParent]) + + const initWs = async () => { + const {data} = await http.pingRoute() + const {user} = data + setCurrentUserId(user.id) + const wsObj = await initWebSocket(user.wsToken) + + setWs(wsObj) + wsObj.onConnect = async () => { + wsObj.subscribe(`/topic/rtc/${user.id}`, async (res: IMessage) => { + const rtcTransportDto = JSON.parse(res.body) as RtcTransportDTO + switch (rtcTransportDto.action) { + case RtcActionEnum.SEND_ANSWER: { + if (rtcTransportDto.answer) { + await peerConnection.setRemoteDescription(new RTCSessionDescription(rtcTransportDto.answer)) + } + break + } + case RtcActionEnum.SEND_OFFER: { + if (rtcTransportDto.offer) { + await peerConnection.setRemoteDescription(new RTCSessionDescription(rtcTransportDto.offer)) + const answer = await peerConnection.createAnswer() + await peerConnection.setLocalDescription(answer) + const answerTransport = new RtcTransportDTO(user.id, "", RtcActionEnum.SEND_ANSWER, undefined, answer) + wsObj.publish({ + destination: `/rtc/${roomUrl}`, + body: JSON.stringify(answerTransport) + }) + } + break + } + case RtcActionEnum.ICE_CANDIDATE: { + if (rtcTransportDto.iceCandidate) { + await peerConnection.addIceCandidate(rtcTransportDto.iceCandidate) + } + break + } + default: + break + } + }) + if (isUserInitiateSession === "join") { + const offer = await peerConnection.createOffer() + await peerConnection.setLocalDescription(offer) + const transport = new RtcTransportDTO(user.id, "", RtcActionEnum.JOIN_ROOM, offer) + wsObj.publish({ + destination: `/rtc/${roomUrl}`, + body: JSON.stringify(transport) + }) + } + } + wsObj.activate() } - try { - const constraints = { - "video": true, - "audio": true - } - const localStream = await navigator.mediaDevices.getUserMedia(constraints) - setLocalStream(localStream) - const videoElement = document.querySelector("video#localVideo") as HTMLVideoElement - localStream.getTracks().forEach((track) => { - peerConnection.addTrack(track, localStream) - }) - if (videoElement) { - setLocalVideoState(true) - videoElement.srcObject = localStream - } - return true - } catch (error) { - // eslint-disable-next-line no-console - console.error("Error accessing media devices.", error) - return false + + const changeVideoStatus = (stopVideo: boolean) => { + if (currentLocalStream) { + if (stopVideo) { + currentLocalStream.getTracks().forEach((track) => { + track.stop() + }) + } else { + currentLocalStream.getTracks().forEach((track) => { + peerConnection.addTrack(track, currentLocalStream) + }) + } + } } - } - - const hangOnRoom = () => { - peerConnection.close() - setCallEnded(true) - if (ws) { - const transport = new RtcTransportDTO(currentUserId, groupUrlFromParent, RtcActionEnum.LEAVE_ROOM) - ws.publish({ - destination: `/app/rtc/${roomUrl}`, - body: JSON.stringify(transport) - }) + + const initRTC = async (): Promise => { + const url = getUuid(location.pathname) + + const urlCheckResponse = await http.ensureRoomExists(url) + setPageStatus(urlCheckResponse.data) + if (urlCheckResponse && !urlCheckResponse.data) { + return false + } + try { + const constraints = { + "video": true, + "audio": true + } + const localStream = await navigator.mediaDevices.getUserMedia(constraints) + setLocalStream(localStream) + const videoElement = document.querySelector("video#localVideo") as HTMLVideoElement + localStream.getTracks().forEach((track) => { + peerConnection.addTrack(track, localStream) + }) + if (videoElement) { + setLocalVideoState(true) + videoElement.srcObject = localStream + } + return true + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error accessing media devices.", error) + return false + } } - if (currentLocalStream) { - currentLocalStream.getTracks().forEach((track) => { - track.stop() - }) + + const hangOnRoom = () => { + peerConnection.close() + setCallEnded(true) + if (ws) { + const transport = new RtcTransportDTO(currentUserId, groupUrlFromParent, RtcActionEnum.LEAVE_ROOM) + ws.publish({ + destination: `/rtc/${roomUrl}`, + body: JSON.stringify(transport) + }) + } + if (currentLocalStream) { + currentLocalStream.getTracks().forEach((track) => { + track.stop() + }) + } } - } - - return ( - - { - isPageAuthorized ? - !callEnded && - <> - - - - - - - - - - - : - - } - { - callEnded && - } - - ) + + return ( + + { + isPageAuthorized ? + !callEnded && + <> + + + + + + + + + + + : + + } + { + callEnded && + } + + ) } diff --git a/frontend-web/src/components/websocket/websocket-chat-component.tsx b/frontend-web/src/components/websocket/websocket-chat-component.tsx index fd86683..a15ce2f 100644 --- a/frontend-web/src/components/websocket/websocket-chat-component.tsx +++ b/frontend-web/src/components/websocket/websocket-chat-component.tsx @@ -1,418 +1,263 @@ -import SendIcon from "@mui/icons-material/Send" -import HighlightOffIcon from "@mui/icons-material/HighlightOff" -import ImageIcon from "@mui/icons-material/Image" -import { Box, Button, CircularProgress, IconButton, Tooltip } from "@mui/material" -import React, { useEffect } from "react" -import { GroupActionEnum } from "./group-action-enum" -import { useAuthContext } from "../../context/auth-context" -import { useLoaderContext } from "../../context/loader-context" -import { useThemeContext } from "../../context/theme-context" -import { useWebSocketContext } from "../../context/ws-context" -import { FullMessageModel } from "../../interface-contract/full-message-model" -import { getPayloadSize } from "../../utils/string-size-calculator" -import { TransportActionEnum } from "../../utils/transport-action-enum" -import { TypeMessageEnum } from "../../utils/type-message-enum" -import { NoDataComponent } from "../partials/no-data-component" -import { TransportModel } from "../../interface-contract/transport-model" -import { CallWindowComponent } from "./call-window-component" -import { ImagePreviewComponent } from "../partials/image-preview" -import { CustomTextField } from "../partials/custom-material-textfield" -import { ActiveVideoCall } from "../partials/video/active-video-call" -import { HttpService } from "../../service/http-service" -import { generateIconColorMode } from "../utils/enable-dark-mode" +import {Box, CircularProgress, Tooltip} from "@mui/material" +import React, {useContext, useEffect, useState} from "react" +import {GroupActionEnum} from "./group-action-enum" +import {useThemeContext} from "../../context/theme-context" +import {FullMessageModel} from "../../interface-contract/full-message-model" +import {TransportActionEnum} from "../../utils/transport-action-enum" +import {TypeMessageEnum} from "../../utils/type-message-enum" +import {TransportModel} from "../../interface-contract/transport-model" +import {ImagePreviewComponent} from "../partials/image-preview" +import {ActiveVideoCall} from "../partials/video/active-video-call" +import {GroupModel} from "../../interface-contract/group-model" +import {NoDataComponent} from "../partials/NoDataComponent" +import {HttpMessageService} from "../../service/http-message.service" +import {AlertAction, AlertContext} from "../../context/AlertContext" +import {CreateMessageComponent} from "../message/CreateMessageComponent" +import {WebSocketContext} from "../../context/WebsocketContext" -export const WebSocketChatComponent: React.FunctionComponent<{ groupUrl: string }> = ({ groupUrl }) => { - const { theme } = useThemeContext() - const { user } = useAuthContext() - const { ws } = useWebSocketContext() - const { setLoading } = useLoaderContext() - const [isPreviewImageOpen, setPreviewImageOpen] = React.useState(false) - const [messageId, setLastMessageId] = React.useState(0) - const [loadingOldMessages, setLoadingOldMessages] = React.useState(false) +export const WebSocketChatComponent: React.FunctionComponent<{ groupUrl?: string }> = ({groupUrl}) => { + const {theme} = useThemeContext() + const {dispatch} = useContext(AlertContext)! + const {ws} = useContext(WebSocketContext)! + const [isPreviewImageOpen, setPreviewImageOpen] = React.useState(false) + const [messageId, setLastMessageId] = React.useState(0) + const [loadingOldMessages, setLoadingOldMessages] = React.useState(false) + const [messages, setMessages] = useState([]) + const [imgSrc, setImgSrc] = React.useState("") + let messageEnd: HTMLDivElement | null - const [imgSrc, setImgSrc] = React.useState("") - const [file, setFile] = React.useState(null) - const [imagePreviewUrl, setImagePreviewUrl] = React.useState("") - const [imageLoaded, setImageLoaded] = React.useState(false) - const [message, setMessage] = React.useState("") - const httpService = new HttpService() - let messageEnd: HTMLDivElement | null - - const chatHistory: any[] = [] - const { - isWsConnected, - allMessagesFetched, - currentActiveGroup, - currentGroup, - userId - } = {} as any + const { + allMessagesFetched, + userId + } = {currentGroup: {} as GroupModel} as any // TODO remove any - useEffect(() => { - if (isWsConnected && groupUrl && ws) { - const transport = new TransportModel(userId || 0, TransportActionEnum.FETCH_GROUP_MESSAGES, undefined, groupUrl, undefined, -1) - ws.publish({ - destination: "/app/message", - body: JSON.stringify(transport) - }) - // setLoading(false) - } - }, [isWsConnected]) - - useEffect(() => { - if (groupUrl && ws && ws.connected) { - const transport = new TransportModel(userId || 0, TransportActionEnum.FETCH_GROUP_MESSAGES, undefined, groupUrl, undefined, -1) - ws.publish({ - destination: "/app/message", - body: JSON.stringify(transport) - }) - } - }, [groupUrl, ws]) + const [groupName, setGroupName] = React.useState("") - useEffect(() => { - if (!loadingOldMessages) { - scrollToEnd() - } - setLoadingOldMessages(false) - if (chatHistory && chatHistory.length > 0) { - setLastMessageId(chatHistory[0].id) - } - }, [chatHistory]) + useEffect(() => { + const fetchMessages = async () => { + if (groupUrl) { + try { + const http = new HttpMessageService() + const {data} = await http.getMessages(groupUrl) + setMessages(data.messages) + setGroupName(data.groupName) + } catch (error) { + dispatch({ + type: AlertAction.ADD_ALERT, + payload: { + id: crypto.randomUUID(), + text: `Cannot fetch messages : ${error}`, + alert: "error", + isOpen: true + } + }) + } + } + } + fetchMessages() + }, [groupUrl]) - function styleSelectedMessage () { - return theme === "dark" ? "hover-msg-dark" : "hover-msg-light" - } + useEffect(() => { + if (!loadingOldMessages) { + scrollToEnd() + } + setLoadingOldMessages(false) + if (messages && messages.length > 0) { + setLastMessageId(messages[0].id) + } + }, [messages]) - function generateImageRender (message: FullMessageModel) { - if (message.fileUrl === undefined) { - return null + function styleSelectedMessage() { + return theme === "dark" ? "hover-msg-dark" : "hover-msg-light" } - return ( -
- {message.name} handleImagePreview(GroupActionEnum.OPEN, message.fileUrl)} - style={{ - border: "1px solid #c8c8c8", - borderRadius: "7%" - }}/> -
- ) - } - - function resetImageBuffer (event: any) { - event.preventDefault() - setFile(null) - setImagePreviewUrl("") - setImageLoaded(false) - } - - function previewFile (event: any) { - resetImageBuffer(event) - const reader = new FileReader() - const file = event.target.files[0] - reader.readAsDataURL(file) - reader.onload = (e) => { - if (e.target && e.target.readyState === FileReader.DONE) { - setFile(file) - setImagePreviewUrl(reader.result as string) - setImageLoaded(true) - } + function generateImageRender(message: FullMessageModel) { + if (message.fileUrl === undefined) { + return null + } + return ( +
+ {message.name} handleImagePreview(GroupActionEnum.OPEN, message.fileUrl)} + style={{ + border: "1px solid #c8c8c8", + borderRadius: "7%" + }}/> +
+ ) } - } - function submitMessage (event: any) { - if (message !== "") { - if (event.key !== undefined && event.shiftKey && event.keyCode === 13) { - return - } - if (event.key !== undefined && event.keyCode === 13) { - event.preventDefault() - setMessage("") - sendMessage() - } + function scrollToEnd() { + messageEnd?.scrollIntoView({behavior: "auto"}) } - } - function handleChange (event: any) { - setMessage(event.target.value) - } - - async function sendMessage () { - if (message !== "") { - if (getPayloadSize(message) < 8192 && ws && ws.active) { - const transport = new TransportModel(userId || 0, TransportActionEnum.SEND_GROUP_MESSAGE, undefined, currentActiveGroup, message) - ws.publish({ - destination: "/app/message", - body: JSON.stringify(transport) - }) - } - setMessage("") - } - if (file !== null) { - const userId = String(user?.id) - const formData = new FormData() - formData.append("file", file) - formData.append("userId", userId) - formData.append("groupUrl", groupUrl || "") - await httpService.uploadFile(formData) - setMessage("") - setImageLoaded(false) - setFile(null) - setImagePreviewUrl("") + function handlePopupState(isOpen: boolean) { + setPreviewImageOpen(isOpen) } - } - - function scrollToEnd () { - messageEnd?.scrollIntoView({ behavior: "auto" }) - } - - function handlePopupState (isOpen: boolean) { - setPreviewImageOpen(isOpen) - } - function handleImagePreview (action: string, src: string) { - switch (action) { - case GroupActionEnum.OPEN: - setImgSrc(src) - handlePopupState(true) - break - case GroupActionEnum.CLOSE: - handlePopupState(false) - break - default: - throw new Error("handleImagePreview failed") + function handleImagePreview(action: string, src: string) { + switch (action) { + case GroupActionEnum.OPEN: + setImgSrc(src) + handlePopupState(true) + break + case GroupActionEnum.CLOSE: + handlePopupState(false) + break + default: + throw new Error("handleImagePreview failed") + } } - } - function markMessageSeen () { - // dispatch(markMessageAsSeen({ - // groupUrl - // })) - } - - function handleScroll (event: any) { - if (event.target.scrollTop === 0) { - if (!allMessagesFetched && ws) { - setLoadingOldMessages(true) - const transport = new TransportModel(userId || 0, TransportActionEnum.FETCH_GROUP_MESSAGES, undefined, groupUrl, undefined, messageId) - ws.publish({ - destination: "/app/message", - body: JSON.stringify(transport) - }) - } - } else { - setLoadingOldMessages(false) + function handleScroll(event: any) { + if (event.target.scrollTop === 0) { + if (!allMessagesFetched && ws) { + setLoadingOldMessages(true) + const transport = new TransportModel(userId || 0, TransportActionEnum.FETCH_GROUP_MESSAGES, undefined, groupUrl, undefined, messageId) + ws.publish({ + destination: "/message", + body: JSON.stringify(transport) + }) + } + } else { + setLoadingOldMessages(false) + } } - } - return ( - <> - { - groupUrl === "" ? -
- -
- : currentGroup.group && -
-
- - {currentGroup.group.name} - - -
-
handleScroll(event)} - style={{ - backgroundColor: "transparent", - width: "100%", - height: "calc(100% - 56px)", - overflowY: "scroll" - }}> - { - !allMessagesFetched && loadingOldMessages && -
-
-
- -
- Loading older messages .... -
-
- } - - {chatHistory && chatHistory.map((messageModel, index, array) => ( - -
- {index >= 1 && array[index - 1].userId === array[index].userId - ?
- :
-
{messageModel.initials}
-
- } -
- {index >= 1 && array[index - 1].userId === array[index].userId - ?
- :
- {messageModel.sender} -
- } - { - messageModel.type === TypeMessageEnum.TEXT - ?
- {messageModel.message} -
- :
- {generateImageRender(messageModel)} -
- } -
-
- - ))} -
{ - messageEnd = el - }}> -
-
-
-
- { - imagePreviewUrl && -
- resetImageBuffer(event)}> - - -
- } -
-
- previewFile(event)} - /> - - - handleChange(event)} - type={"text"} - keyUp={submitMessage} - isMultiline={true} - isDarkModeEnable={theme} - name={"mainWriteMessage"}/> - -
-
-
- } - - ) + return ( + <> + { + !groupUrl ? +
+ +
+ : groupName && +
+
+ + +
{groupName}
+
+ +
+
+
handleScroll(event)} + style={{ + backgroundColor: "white", + display: "flex", + flexDirection: "column", + justifyContent: "space-between", + width: "100%", + height: "100%", + }}> + + { + !allMessagesFetched && loadingOldMessages && +
+
+
+ +
+ Loading older messages .... +
+
+ } + + {messages.map((messageModel, index, array) => ( + +
+ {index >= 1 && array[index - 1].userId === array[index].userId + ?
+ :
+
{messageModel.initials}
+
+ } +
+ {index >= 1 && array[index - 1].userId === array[index].userId + ?
+ :
+ {messageModel.sender} +
+ } + { + messageModel.type === TypeMessageEnum.TEXT + ?
+ {messageModel.message} +
+ :
+ {generateImageRender(messageModel)} +
+ } +
+
+ + ))} + {/*
{*/} + {/* messageEnd = el*/} + {/* }}>*/} + {/*
*/} + +
+
+ } + + ) } diff --git a/frontend-web/src/components/websocket/websocket-group-actions-component.tsx b/frontend-web/src/components/websocket/websocket-group-actions-component.tsx index f13b154..bdfbdaa 100644 --- a/frontend-web/src/components/websocket/websocket-group-actions-component.tsx +++ b/frontend-web/src/components/websocket/websocket-group-actions-component.tsx @@ -16,22 +16,21 @@ import PersonIcon from "@mui/icons-material/Person" import GroupAddIcon from "@mui/icons-material/GroupAdd" import GroupIcon from "@mui/icons-material/Group" import MoreHorizIcon from "@mui/icons-material/MoreHoriz" -import React, {useEffect, useState} from "react" +import React, {useContext, useState} from "react" import {GroupActionEnum} from "./group-action-enum" -import {useAuthContext} from "../../context/auth-context" import {useThemeContext} from "../../context/theme-context" import { generateClassName, generateIconColorMode } from "../utils/enable-dark-mode" -import {useWebSocketContext} from "../../context/ws-context" import {TransportModel} from "../../interface-contract/transport-model" import {TransportActionEnum} from "../../utils/transport-action-enum" import {AllUsersDialog} from "../partials/all-users-dialog" import {HttpService} from "../../service/http-service" import {GroupUserModel} from "../../interface-contract/group-user-model" +import {WebSocketContext} from "../../context/WebsocketContext" -export const WebSocketGroupActionComponent: React.FunctionComponent<{ groupUrl: string }> = ({groupUrl}) => { +export const WebSocketGroupActionComponent: React.FunctionComponent<{ groupUrl?: string }> = ({groupUrl}) => { const [paramsOpen, setParamsOpen] = useState(false) const [popupOpen, setPopupOpen] = useState(false) const [usersInConversation, setUsersInConversation] = useState([]) @@ -40,18 +39,11 @@ export const WebSocketGroupActionComponent: React.FunctionComponent<{ groupUrl: const [toolTipAction, setToolTipAction] = useState(false) const [openTooltipId, setToolTipId] = useState(null) const {theme} = useThemeContext() - const {ws} = useWebSocketContext() + const {ws} = useContext(WebSocketContext)! const httpService = new HttpService() - const {user} = useAuthContext() + const {user} = {} as any // TODO remove any const groups = [] - const currentActiveGroup = {} as any - useEffect(() => { - clearData() - return () => { - clearData() - } - }, [currentActiveGroup]) function handleTooltipAction(event: any, action: string) { event.preventDefault() @@ -64,16 +56,6 @@ export const WebSocketGroupActionComponent: React.FunctionComponent<{ groupUrl: } } - function clearData() { - setAllUsers([]) - setToolTipAction(false) - setToolTipId(null) - setCurrentUserIsAdmin(false) - setUsersInConversation([]) - handlePopupState(false) - setParamsOpen(false) - } - function handleDisplayUserAction(event: any, id: number) { event.preventDefault() setToolTipId(id) @@ -90,7 +72,7 @@ export const WebSocketGroupActionComponent: React.FunctionComponent<{ groupUrl: switch (action) { case GroupActionEnum.PARAM: if (usersInConversation.length === 0) { - const res = await httpService.fetchAllUsersInConversation(groupUrl) + const res = await httpService.fetchAllUsersInConversation(groupUrl || "") res.data.forEach((groupUserModel) => { if (groupUserModel.userId === user?.id && groupUserModel.admin) { setCurrentUserIsAdmin(true) @@ -114,7 +96,7 @@ export const WebSocketGroupActionComponent: React.FunctionComponent<{ groupUrl: case GroupActionEnum.OPEN: handlePopupState(true) if (allUsers.length === 0) { - const res = await httpService.fetchAllUsersWithoutAlreadyInGroup(groupUrl) + const res = await httpService.fetchAllUsersWithoutAlreadyInGroup(groupUrl || "") setAllUsers(res.data) } break @@ -130,7 +112,7 @@ export const WebSocketGroupActionComponent: React.FunctionComponent<{ groupUrl: if (ws) { const transport = new TransportModel(userId, TransportActionEnum.LEAVE_GROUP, undefined, groupUrl) ws.publish({ - destination: "/app/message", + destination: "/message", body: JSON.stringify(transport) }) } @@ -138,7 +120,7 @@ export const WebSocketGroupActionComponent: React.FunctionComponent<{ groupUrl: async function removeUserFromAdminListInConversation(userId: string | number) { try { - const res = await httpService.removeAdminUserInConversation(userId, groupUrl) + // const res = await httpService.removeAdminUserInConversation(userId, groupUrl) const users = [...usersInConversation] const user = users.find((elt) => elt.userId === userId) if (user) { @@ -165,7 +147,7 @@ export const WebSocketGroupActionComponent: React.FunctionComponent<{ groupUrl: async function grantUserAdminInConversation(userId: number | string) { try { - const res = await httpService.grantUserAdminInConversation(userId, groupUrl) + // const res = await httpService.grantUserAdminInConversation(userId, groupUrl) // dispatch(setAlerts({ // alert: { // text: res.data, @@ -192,7 +174,7 @@ export const WebSocketGroupActionComponent: React.FunctionComponent<{ groupUrl: async function addUserInConversation(userId: string | number) { try { - const res = await httpService.addUserToGroup(userId, groupUrl) + const res = await httpService.addUserToGroup(userId, groupUrl || "") const users = [...usersInConversation] users.push(res.data) setUsersInConversation(users) @@ -218,7 +200,7 @@ export const WebSocketGroupActionComponent: React.FunctionComponent<{ groupUrl: async function removeUserFromConversation(userId: string | number) { try { - const res = await httpService.removeUserFromConversation(userId, groupUrl) + // const res = await httpService.removeUserFromConversation(userId, groupUrl) // dispatch(setAlerts({ // alert: { // text: res.data, @@ -242,7 +224,7 @@ export const WebSocketGroupActionComponent: React.FunctionComponent<{ groupUrl: } return ( -
+
diff --git a/frontend-web/src/components/websocket/websocket-groups-component.tsx b/frontend-web/src/components/websocket/websocket-groups-component.tsx index db307e9..7a0c866 100644 --- a/frontend-web/src/components/websocket/websocket-groups-component.tsx +++ b/frontend-web/src/components/websocket/websocket-groups-component.tsx @@ -2,22 +2,23 @@ import AccountCircleIcon from "@mui/icons-material/AccountCircle" import ArrowRightAltIcon from "@mui/icons-material/ArrowRightAlt" import ErrorIcon from "@mui/icons-material/Error" import FolderIcon from "@mui/icons-material/Folder" -import {Alert, Avatar, Collapse, List, ListItemButton, ListItemText} from "@mui/material" -import React, {useEffect, useState} from "react" -import {Link} from "react-router-dom" +import {Alert, Avatar, Box, Button, Collapse, List, ListItemButton, ListItemText} from "@mui/material" +import React, {useContext, useEffect, useState} from "react" +import {Link, useNavigate} from "react-router-dom" import {useThemeContext} from "../../context/theme-context" import {generateColorMode, generateLinkColorMode} from "../utils/enable-dark-mode" import {TypeGroupEnum} from "../../utils/type-group-enum" import {dateParser} from "../../utils/date-formater" import {SkeletonLoader} from "../partials/skeleten-loader" -import {useLoaderContext} from "../../context/loader-context" +import {AuthUserContext} from "../../context/AuthContext" +import NoteAddOutlinedIcon from "@mui/icons-material/NoteAddOutlined" interface IClockType { date: string } interface IWebSocketGroupComponent { - groupUrl: string + groupUrl?: string } const Clock: React.FunctionComponent = ({date}) => { @@ -41,16 +42,14 @@ const Clock: React.FunctionComponent = ({date}) => { } export const WebsocketGroupsComponent: React.FunctionComponent = ({groupUrl}) => { - const { - setLoading - } = useLoaderContext() const [loadingState, setLoadingState] = React.useState(true) const {theme} = useThemeContext() - const groups: any[] = [] - const isWsConnected = false + const navigate = useNavigate() + const {groups} = useContext(AuthUserContext)! + const isWsConnected = true function changeGroupName(url: string) { - const currentGroup = groups.find((elt) => elt.group.url === url) + const currentGroup = groups.find((group) => group.url === url) if (currentGroup) { // dispatch(setCurrentGroup({currentGroup})) } @@ -59,7 +58,7 @@ export const WebsocketGroupsComponent: React.FunctionComponent { if (groups) { if (groups.length !== 0) { - changeGroupName(groupUrl) + changeGroupName(groupUrl || "") // dispatch(setCurrentGroup({currentGroup: groups[0]})) // dispatch(setCurrentActiveGroup({currentActiveGroup: groupUrl})) } @@ -73,7 +72,7 @@ export const WebsocketGroupsComponent: React.FunctionComponent - +
Application is currently unavailable @@ -105,7 +98,6 @@ export const WebsocketGroupsComponent: React.FunctionComponent
+ + + { !loadingState && groups && groups.length === 0 &&

@@ -140,29 +136,31 @@ export const WebsocketGroupsComponent: React.FunctionComponent

} - - {!loadingState && groups && groups.map((groupWrapper) => ( - redirectToGroup(groupWrapper.group.id, groupWrapper.group.url)}> - - { - groupWrapper.group.groupType === TypeGroupEnum.GROUP - ? - : - } - - + + + {!loadingState && groups && groups.map((group) => ( + redirectToGroup(group.id, group.url)}> + + { + group.groupType === TypeGroupEnum.GROUP + ? + : + } + + {groupWrapper.group.name} + className={styleUnreadMessage(!group.lastMessageSeen)}>{group.name} - } - secondary={ - + } + secondary={ + - {groupWrapper.group.lastMessageSender ? groupWrapper.group.lastMessageSender + ": " : ""} - {groupWrapper.group.lastMessage - ? groupWrapper.group.lastMessage + {group.lastMessageSender ? group.lastMessageSender + ": " : ""} + {group.lastMessage + ? group.lastMessage : No message for the moment} - {groupWrapper.group.lastMessage ? + {group.lastMessage ? ยท : ""} { - groupWrapper.group.lastMessage && - + group.lastMessage && + } - } - /> - - ))} - { - loadingState && - } - + } + /> + + ))} + { + loadingState && + } + + +
) } diff --git a/frontend-web/src/components/websocket/websocket-main-component.tsx b/frontend-web/src/components/websocket/websocket-main-component.tsx index 989b66b..e22ab3b 100644 --- a/frontend-web/src/components/websocket/websocket-main-component.tsx +++ b/frontend-web/src/components/websocket/websocket-main-component.tsx @@ -1,50 +1,33 @@ import "./websocketStyle.css" -import React, { useEffect } from "react" -import { WebSocketChatComponent } from "./websocket-chat-component" -import { WebSocketGroupActionComponent } from "./websocket-group-actions-component" -import { WebsocketGroupsComponent } from "./websocket-groups-component" -import { useThemeContext } from "../../context/theme-context" -import { generateColorMode } from "../utils/enable-dark-mode" -import { useWebSocketContext, WebsocketContextProvider } from "../../context/ws-context" +import React, {useEffect} from "react" +import {WebSocketChatComponent} from "./websocket-chat-component" +import {WebSocketGroupActionComponent} from "./websocket-group-actions-component" +import {WebsocketGroupsComponent} from "./websocket-groups-component" +import {useThemeContext} from "../../context/theme-context" +import {generateColorMode} from "../utils/enable-dark-mode" +import {useParams} from "react-router-dom" +import {WebsocketContextProvider} from "../../context/WebsocketContext" -export const WebSocketMainComponent: React.FunctionComponent = (): JSX.Element => { - const { theme } = useThemeContext() - const { ws } = useWebSocketContext() - const [groupUrlProps, setGroupUrl] = React.useState("") - const currentActiveGroup = "" +export const WebSocketMainComponent: React.FunctionComponent = (): React.JSX.Element => { + const {theme} = useThemeContext() + const {groupId} = useParams() - useEffect(() => { - if (currentActiveGroup) { - setGroupUrl(currentActiveGroup) - } else { - const groupUrl = window.location.pathname.split("/").slice(-1)[0] - if (groupUrl) { - setGroupUrl(groupUrl) - } - } - }, [currentActiveGroup]) + useEffect(() => { + document.title = "Messages | FLM" + }, []) - useEffect(() => { - document.title = "Messages | FLM" - return () => { - if (ws) { - ws.deactivate() - } - } - }, []) - - return ( -
- - - - - -
- ) + return ( +
+ + + + + +
+ ) } diff --git a/frontend-web/src/config/websocket-config.ts b/frontend-web/src/config/websocket-config.ts index 220c745..0863aec 100644 --- a/frontend-web/src/config/websocket-config.ts +++ b/frontend-web/src/config/websocket-config.ts @@ -1,19 +1,20 @@ -import { Client } from "@stomp/stompjs" +import {Client} from "@stomp/stompjs" +import {HttpService} from "../service/http-service" -const WS_URL = process.env.NODE_ENV === "development" ? "localhost:9090/" : "localhost:9090/" +const WS_URL = process.env.NODE_ENV === "development" ? "localhost:9090/api/" : "localhost:9090/api/" const WS_BROKER = process.env.NODE_ENV === "development" ? "ws" : "wss" -export function initWebSocket (userToken: string): Client { - return new Client({ - brokerURL: `${WS_BROKER}://${WS_URL}messenger/websocket?token=${userToken}`, - // Uncomment lines to activate WS debug - // debug: (str: string) => { - // console.log(str); - // }, - connectHeaders: { clientSessionId: crypto.randomUUID() }, - reconnectDelay: 5000, - heartbeatIncoming: 4000, - heartbeatOutgoing: 4000 - }) +export async function initWebSocket(userToken: string): Promise { + console.log("Initiating WS connection...") + const service = new HttpService() + const {data} = await service.getCsrfToken() + const {headerName, token} = data + return new Client({ + brokerURL: `${WS_BROKER}://${WS_URL}messenger/websocket?token=${userToken}`, + connectHeaders: {clientSessionId: crypto.randomUUID(), [headerName]: token}, + reconnectDelay: 5000, + heartbeatIncoming: 4000, + heartbeatOutgoing: 4000 + }) } diff --git a/frontend-web/src/context/AlertContext.tsx b/frontend-web/src/context/AlertContext.tsx new file mode 100644 index 0000000..168cf5f --- /dev/null +++ b/frontend-web/src/context/AlertContext.tsx @@ -0,0 +1,52 @@ +import React, {createContext, Dispatch, ReactNode, useReducer} from "react" + +enum AlertAction { + DELETE_ALERT = "DELETE_ALERT", + ADD_ALERT = "ADD_ALERT" +} + +type AlertType = { + id: string, + isOpen: boolean, + text: string, + alert: "success" | "info" | "warning" | "error" | undefined +} + +export type AlertActionType = + | { type: AlertAction.ADD_ALERT; payload: AlertType } + | { type: AlertAction.DELETE_ALERT; payload: string }; + +export const AlertContext = createContext<{ + alerts: AlertType[]; + dispatch: Dispatch; +} | null>(null) + +export const alertsReducer = (state: AlertType[], action: AlertActionType): AlertType[] => { + switch (action.type) { + case AlertAction.ADD_ALERT: { + return [...state, action.payload] + } + + case AlertAction.DELETE_ALERT: { + const indexToDelete = state.findIndex((alert) => alert.id === action.payload) + const eltToDelete = state[indexToDelete] + eltToDelete.isOpen = false + state[indexToDelete] = eltToDelete + return state.filter((alert) => alert.id !== action.payload) + } + + default: + return state + } +} + +const AlertContextProvider: React.FC<{ children: ReactNode }> = ({children}) => { + const [alerts, dispatch] = useReducer(alertsReducer, []) + return ( + + {children} + + ) +} + +export {AlertContextProvider, AlertAction} diff --git a/frontend-web/src/context/AuthContext.tsx b/frontend-web/src/context/AuthContext.tsx new file mode 100644 index 0000000..a316d9f --- /dev/null +++ b/frontend-web/src/context/AuthContext.tsx @@ -0,0 +1,34 @@ +import React, {createContext, useEffect, useState} from "react" +import {IUser} from "../interface-contract/user/user-model" +import {HttpService} from "../service/http-service" +import {GroupModel} from "../interface-contract/group-model" + +type AuthUserContextType = { + user: IUser | undefined; + groups: GroupModel[] + setUser: (user: IUser | undefined) => void + setGroups: (groups: GroupModel[]) => void +}; + +const AuthUserContext = createContext(null) + +const AuthUserContextProvider: React.FC<{ children: React.ReactNode }> = ({children}) => { + const [user, setUser] = useState(undefined) + const [groups, setGroups] = useState([]) + useEffect(() => { + const getUserData = async () => { + const userData = await new HttpService().pingRoute() + setUser(userData.data.user) + setGroups(userData.data.groupsWrapper.map((group => group.group))) + } + getUserData() + }, []) + return ( + + {children} + + ) +} + + +export {AuthUserContext, AuthUserContextProvider} diff --git a/frontend-web/src/context/WebsocketContext.tsx b/frontend-web/src/context/WebsocketContext.tsx new file mode 100644 index 0000000..d87b040 --- /dev/null +++ b/frontend-web/src/context/WebsocketContext.tsx @@ -0,0 +1,131 @@ +import {Client, IMessage} from "@stomp/stompjs" +import React, {createContext, ReactNode, useContext, useEffect, useState} from "react" +import {playNotificationSound} from "../components/utils/play-sound-notification" +import {initWebSocket} from "../config/websocket-config" +import {FullMessageModel} from "../interface-contract/full-message-model" +import {OutputTransportDTO} from "../interface-contract/input-transport-model" +import {TransportActionEnum} from "../utils/transport-action-enum" +import {IUser} from "../interface-contract/user/user-model" +import {AuthUserContext} from "./AuthContext" + +type WebSocketContextType = { + ws: Client | undefined + isWsConnected: boolean + setWsClient: (ws: Client) => void + setWsConnected: (isConnected: boolean) => void +} + +const WebSocketContext = createContext(undefined) + +const WebsocketContextProvider: React.FC<{ children: ReactNode }> = ({children}) => { + const [ws, setWsClient] = useState(undefined) + const [isWsConnected, setWsConnected] = useState(false) + const {user} = useContext(AuthUserContext)! + + useEffect(() => { + if (user && user.wsToken !== null) { + initWs(user) + } + }, [user]) + + async function initWs(user: IUser) { + const wsObj = await initWebSocket(user.wsToken) + setWsClient(wsObj) + wsObj.onConnect = () => { + console.log("WS connected") + // dispatch(wsHealthCheckConnected({ isWsConnected: true })) + // setLoading(false) + wsObj.subscribe(`/topic/user/${user.id}`, (res: IMessage) => { + const data = JSON.parse(res.body) as OutputTransportDTO + switch (data.action) { + case TransportActionEnum.FETCH_GROUP_MESSAGES: { + // const result = data.object as WrapperMessageModel + // dispatch(setGroupMessages({ messages: result.messages })) + // dispatch(setAllMessagesFetched({ + // allMessagesFetched: result.lastMessage + // })) + break + } + case TransportActionEnum.LEAVE_GROUP: { + // const { + // groupUrl, + // groupName + // } = data.object as ILeaveGroupModel + // dispatch(removeUserFromGroup({ groupUrl })) + // dispatch(setAlerts({ + // alert: { + // alert: "success", + // isOpen: true, + // text: `you left the group ${groupName}` + // } + // })) + break + } + case TransportActionEnum.ADD_CHAT_HISTORY: { + // const wrapper = data.object as WrapperMessageModel + // dispatch(setAllMessagesFetched({ allMessagesFetched: wrapper.lastMessage })) + // const messages = wrapper.messages + // dispatch(setGroupMessages({ messages })) + } + break + case TransportActionEnum.SEND_GROUP_MESSAGE: + break + case TransportActionEnum.NOTIFICATION_MESSAGE: { + const message = data.object as FullMessageModel + // dispatch(updateGroupsWithLastMessageSent({ + // userId: user.id, + // message + // })) + // updateGroupsWithLastMessageSent(dispatch, groups, message, user.id) + // dispatch(addChatHistory({ newMessage: message })) + if (message.userId !== user.id) { + playNotificationSound() + } + } + break + case TransportActionEnum.CALL_INCOMING: + // dispatch(setCallIncoming({ callStarted: true })) + // dispatch(setCallUrl({ + // callUrl: data.object as unknown as string + // })) + break + case TransportActionEnum.END_CALL: { + // const groupUrl = data.object as unknown as string + // dispatch(setGroupWithCurrentCall({ groupUrl })) + break + } + default: + break + } + }) + + } + + // wsObj.onWebSocketClose = (evt) => { + // console.log("ERROR DURING HANDSHAKE WITH SERVER", evt) + // dispatch(wsHealthCheckConnected({ isWsConnected: false })) + // } + + wsObj.onStompError = (error) => { + console.error("Cannot connect to STOMP server", error) + } + + wsObj.onWebSocketError = (evt) => { + console.log("Cannot connect to server", evt) + } + wsObj.activate() + } + + return ( + + {children} + + ) +} + +export {WebSocketContext, WebsocketContextProvider} diff --git a/frontend-web/src/context/auth-context.tsx b/frontend-web/src/context/auth-context.tsx deleted file mode 100644 index 911ece0..0000000 --- a/frontend-web/src/context/auth-context.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { useContext, useEffect, useState } from "react" -import { HttpService } from "../service/http-service" -import { IUser } from "../interface-contract/user/user-model" - -type AuthContextType = { - user: IUser | undefined - setUser: (user: IUser | undefined) => void -} - -export const AuthContext = React.createContext({} as AuthContextType) - -export const AuthContextProvider: React.FunctionComponent = ({ children }) => { - const [user, setUser] = useState() - - useEffect(() => { - async function authInit () { - try { - - const response = await new HttpService().pingRoute() - const { user } = response.data - setUser(user) - // dispatch(setUserWsToken({ wsToken: user.wsToken })) - // dispatch(setUserId({ userId: user.id })) - // dispatch(setWsUserGroups({ groups: groupsWrapper })) - } catch (error) { - // dispatch(setAlerts({ - // alert: { - // isOpen: true, - // alert: "warning", - // text: "You are not authenticated." - // } - // })) - } finally { - // dispatch(setAuthLoading({ isLoading: false })) - } - } - - authInit() - }, []) - - return ( - - {children} - - ) -} - -export const useAuthContext = (): AuthContextType => useContext(AuthContext) diff --git a/frontend-web/src/context/loader-context.tsx b/frontend-web/src/context/loader-context.tsx index 22cb190..a0709f9 100644 --- a/frontend-web/src/context/loader-context.tsx +++ b/frontend-web/src/context/loader-context.tsx @@ -1,22 +1,22 @@ -import React, { useContext } from "react" - +import React, {createContext, useState} from "react" type LoaderContextType = { loading: boolean; - setLoading: (isLoading: boolean) => void + setLoading: (isAppLoading: boolean) => void }; -const LoaderContext = React.createContext( +const LoaderContext = createContext( {} as LoaderContextType ) -// export const LoaderProvider: React.FunctionComponent = ({ children }) => { -// const [loading, setLoading] = useState(defaultStatus) -// return ( -// -// { children } -// -// ) -// } +const LoaderProvider: React.FC<{children: React.ReactNode}> = ({children}) => { + const [loading, setLoading] = useState(false) + return ( + + {children} + + ) +} + -export const useLoaderContext = (): LoaderContextType => useContext(LoaderContext) +export {LoaderContext, LoaderProvider} diff --git a/frontend-web/src/context/ws-context.tsx b/frontend-web/src/context/ws-context.tsx deleted file mode 100644 index 263ca76..0000000 --- a/frontend-web/src/context/ws-context.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { Client, IMessage } from "@stomp/stompjs" -import React, { useContext, useEffect, useState } from "react" -import { useAuthContext } from "./auth-context" -import { useLoaderContext } from "./loader-context" -import { playNotificationSound } from "../components/utils/play-sound-notification" -import { initWebSocket } from "../config/websocket-config" -import { FullMessageModel } from "../interface-contract/full-message-model" -import { OutputTransportDTO } from "../interface-contract/input-transport-model" -import { WrapperMessageModel } from "../interface-contract/wrapper-message-model" -import { StoreState } from "../reducers/types" -import { TransportActionEnum } from "../utils/transport-action-enum" -import { ILeaveGroupModel } from "../interface-contract/leave-group-model" -import { IUser } from "../interface-contract/user/user-model" - -type WebSocketContextType = { - ws: Client | undefined - setWsClient: (ws: Client) => void -} - -export const WebSocketContext = React.createContext({} as WebSocketContextType) - -export const WebsocketContextProvider: React.FunctionComponent = ({ children }) => { - const [ws, setWsClient] = useState(undefined) - const { user } = useAuthContext() - const { setLoading } = useLoaderContext() - - useEffect(() => { - if (user && user.wsToken !== null) { - setLoading(true) - initWs(user) - } - return () => { - // wsHealthCheckConnected({ isWsConnected: false })) - } - }) - - async function initWs (user: IUser) { - const wsObj = await initWebSocket(user.wsToken) - setWsClient(wsObj) - wsObj.onConnect = () => { - // dispatch(wsHealthCheckConnected({ isWsConnected: true })) - // setLoading(false) - wsObj.subscribe(`/topic/user/${user.id}`, (res: IMessage) => { - const data = JSON.parse(res.body) as OutputTransportDTO - switch (data.action) { - case TransportActionEnum.FETCH_GROUP_MESSAGES: { - const result = data.object as WrapperMessageModel - // dispatch(setGroupMessages({ messages: result.messages })) - // dispatch(setAllMessagesFetched({ - // allMessagesFetched: result.lastMessage - // })) - break - } - case TransportActionEnum.LEAVE_GROUP: { - const { - groupUrl, - groupName - } = data.object as ILeaveGroupModel - // dispatch(removeUserFromGroup({ groupUrl })) - // dispatch(setAlerts({ - // alert: { - // alert: "success", - // isOpen: true, - // text: `you left the group ${groupName}` - // } - // })) - break - } - case TransportActionEnum.ADD_CHAT_HISTORY: { - const wrapper = data.object as WrapperMessageModel - // dispatch(setAllMessagesFetched({ allMessagesFetched: wrapper.lastMessage })) - const messages = wrapper.messages - // dispatch(setGroupMessages({ messages })) - } - break - case TransportActionEnum.SEND_GROUP_MESSAGE: - break - case TransportActionEnum.NOTIFICATION_MESSAGE: { - const message = data.object as FullMessageModel - // dispatch(updateGroupsWithLastMessageSent({ - // userId: user.id, - // message - // })) - // updateGroupsWithLastMessageSent(dispatch, groups, message, user.id) - // dispatch(addChatHistory({ newMessage: message })) - if (message.userId !== user.id) { - playNotificationSound() - } - } - break - case TransportActionEnum.CALL_INCOMING: - // dispatch(setCallIncoming({ callStarted: true })) - // dispatch(setCallUrl({ - // callUrl: data.object as unknown as string - // })) - break - case TransportActionEnum.END_CALL: { - const groupUrl = data.object as unknown as string - // dispatch(setGroupWithCurrentCall({ groupUrl })) - break - } - default: - break - } - }) - - } - - wsObj.onWebSocketClose = (evt) => { - console.log("ERROR DURING HANDSHAKE WITH SERVER", evt) - // dispatch(wsHealthCheckConnected({ isWsConnected: false })) - } - - wsObj.onWebSocketError = (evt) => { - console.log("Cannot connect to server", evt) - } - wsObj.activate() - } - - return ( - - {children} - - ) -} - -export const useWebSocketContext = (): WebSocketContextType => useContext(WebSocketContext) diff --git a/frontend-web/src/index.css b/frontend-web/src/index.css index 3bbeafb..10dbd72 100644 --- a/frontend-web/src/index.css +++ b/frontend-web/src/index.css @@ -9,7 +9,95 @@ body, html { -moz-osx-font-smoothing: grayscale; } +#root { + height: 100%; +} + +.msg { + white-space: pre-wrap; + padding: 2px 0 2px 5px; +} + +.bold-unread-message-light { + font-weight: bold !important; + color: white !important; +} + +.bold-unread-message-dark { + font-weight: bold !important; + color: black !important; +} + +.hover-msg-light:hover { + background-color: #dce9ff !important; +} + +.hover-msg-dark:hover { + background-color: #262626; +} + +.selected-group-light { + background-color: #dce9ff !important; +} + +.selected-group-dark { + background-color: #262626 !important; +} + +.group-subtitle-color { + color: #787878 +} + +.lnk { + text-decoration: none; +} + +.mnu { + display: flex; +} + +.clrcstm { + color: inherit; + font-weight: inherit; +} + code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } + +.dark { + color: white; + background-color: #393939; +} + +.light { + color: black; + background-color: #ffffff; + /*background-color: #A9A9A9*/ +} + +.jsLink { + color: white; +} + + +::-webkit-scrollbar { + width: 10px; +} + +::-webkit-scrollbar-track { + background: #656565; +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background: #c4c4c4; +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover { + background: #525252; +} + + diff --git a/frontend-web/src/index.tsx b/frontend-web/src/index.tsx index 1a15ba4..69f1ec3 100644 --- a/frontend-web/src/index.tsx +++ b/frontend-web/src/index.tsx @@ -1,45 +1,62 @@ import React from "react" import "./index.css" import * as serviceWorker from "./serviceWorker" -import {createBrowserRouter, RouterProvider} from "react-router-dom" +import {createBrowserRouter, redirect, RouterProvider} from "react-router-dom" import {HomeComponent} from "./components/home" import {createRoot} from "react-dom/client" -import {CreateGroupComponent} from "./components/create-group/create-group-component" import {RegisterFormComponent} from "./components/register/register-user" - -// ReactDOM.render( -// -// -// -// -// -// -// -// -// -// -// , -// document.getElementById("root") -// ) +import {WebSocketMainComponent} from "./components/websocket/websocket-main-component" +import {VideoComponent} from "./components/websocket/video-component" +import {LoaderProvider} from "./context/loader-context" +import {AlertComponent} from "./components/partials/alert-component" +import {LoaderComponent} from "./components/partials/loader/LoaderComponent" +import {HttpService} from "./service/http-service" +import {AlertContextProvider} from "./context/AlertContext" +import {AuthUserContextProvider} from "./context/AuthContext" +import {HeaderComponent} from "./components/partials/HeaderComponent" const router = createBrowserRouter([ { path: "/", - element: , + loader: async () => { + return new HttpService().pingRoute().catch(() => redirect("/login")) + }, }, { - path: "create", - element: , + path: "login", + element: , }, { path: "register", element: + }, + { + path: "t/messages", + element: + }, + { + path: "t/messages/:groupId", + element: + }, + { + path: "call/:uuid", + element: } ]) -createRoot(document.getElementById("root")!).render( - -) +createRoot(document.getElementById("root")!) + .render( + + + + + + + + + + + ) // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. diff --git a/frontend-web/src/interface-contract/csrf/csrf.type.ts b/frontend-web/src/interface-contract/csrf/csrf.type.ts new file mode 100644 index 0000000..e431a29 --- /dev/null +++ b/frontend-web/src/interface-contract/csrf/csrf.type.ts @@ -0,0 +1,5 @@ +export type Csrf = { + parameterName: string, + token: string, + headerName: string, +} diff --git a/frontend-web/src/interface-contract/wrapper-message-model.ts b/frontend-web/src/interface-contract/wrapper-message-model.ts index e7415a0..05b1a17 100644 --- a/frontend-web/src/interface-contract/wrapper-message-model.ts +++ b/frontend-web/src/interface-contract/wrapper-message-model.ts @@ -3,4 +3,5 @@ import { FullMessageModel } from "./full-message-model" export interface WrapperMessageModel { lastMessage: boolean messages: FullMessageModel[] + groupName: string } diff --git a/frontend-web/src/service/http-main.service.ts b/frontend-web/src/service/http-main.service.ts new file mode 100644 index 0000000..d6d115d --- /dev/null +++ b/frontend-web/src/service/http-main.service.ts @@ -0,0 +1,20 @@ +import axios, {AxiosInstance} from "axios" + +export abstract class HttpMainService { + + protected instance: AxiosInstance + + protected constructor() { + const baseURL = process.env.NODE_ENV === "development" ? "http://localhost:9090/api" : "http://production-url.com/api" + this.instance = axios.create({ + withCredentials: true, + baseURL + }) + this.instance.interceptors.response.use((response) => { + return response + }, (error) => { + console.log("ERROR", error) + return Promise.reject(error) + }) + } +} diff --git a/frontend-web/src/service/http-message.service.ts b/frontend-web/src/service/http-message.service.ts new file mode 100644 index 0000000..1c111f1 --- /dev/null +++ b/frontend-web/src/service/http-message.service.ts @@ -0,0 +1,13 @@ +import {HttpMainService} from "./http-main.service" +import {WrapperMessageModel} from "../interface-contract/wrapper-message-model" +import {AxiosResponse} from "axios" + +export class HttpMessageService extends HttpMainService { + public constructor() { + super() + } + + public getMessages(groupUrl: string): Promise> { + return this.instance.get(`/messages/group/${groupUrl}`) + } +} diff --git a/frontend-web/src/service/http-service.ts b/frontend-web/src/service/http-service.ts index 41f1b17..ff5e322 100644 --- a/frontend-web/src/service/http-service.ts +++ b/frontend-web/src/service/http-service.ts @@ -1,94 +1,80 @@ -import axios, { AxiosInstance, AxiosResponse } from "axios" -import { GroupModel } from "../interface-contract/group-model" -import { IUserWrapper } from "../interface-contract/user/user-wrapper" -import { JwtModel } from "../interface-contract/jwt-model" -import { IUser } from "../interface-contract/user/user-model" -import { GroupUserModel } from "../interface-contract/group-user-model" - -export class HttpService { - - private instance: AxiosInstance - - constructor () { - const baseURL = process.env.NODE_ENV === "development" ? "http://localhost:9090/api" : "http://production-url.com/api" - this.instance = axios.create({ - withCredentials: true, - baseURL - }) - this.instance.interceptors.response.use((response) => { - return response - }, (error) => { - if (error.message === "Network Error") { - // store.dispatch(setAlerts({ - // alert: { - // text: "Unable to reach server. Please try again later.", - // alert: "error", - // isOpen: true - // } - // })) - } - return Promise.reject(error) - }) - } - - authenticate (jwtModel: JwtModel): Promise> { - return this.instance.post("auth", jwtModel) - } - - public async pingRoute (): Promise> { - return this.instance.get("fetch") - } - - public async ensureRoomExists (roomId: string): Promise> { - return await axios.get(`http://localhost:9090/room/ensure-room-exists/${roomId}`, { withCredentials: true }) - } - - public logout (): Promise { - return this.instance.get("logout") - } - - public createGroup (groupName: string): Promise> { - return this.instance.post("create", { name: groupName }) - } - - public addUserToGroup (userId: number | string, groupUrl: string): Promise { - return this.instance.get("user/add/" + userId + "/" + groupUrl) - } - - public fetchAllUsersInConversation (groupUrl: string): Promise> { - return this.instance.get("users/group/" + groupUrl, {}) - } - - public fetchAllUsersWithoutAlreadyInGroup (groupUrl: string): Promise> { - return this.instance.get("users/all/" + groupUrl, {}) - } - - createUser (firstname: string, lastname: string, email: string, password: string): Promise { - return this.instance.post("user/register", { - firstname, - lastname, - email, - password - }) - } - - public leaveConversation (userIdToRemove: number, groupId: string): Promise { - return this.instance.get("user/leave/" + userIdToRemove + "/group/" + groupId) - } - - public removeUserFromConversation (userIdToRemove: string | number, groupUrl: string): Promise { - return this.instance.get("user/remove/" + userIdToRemove + "/group/" + groupUrl) - } - - public removeAdminUserInConversation (userIdToRemove: string | number, groupUrl: string): Promise { - return this.instance.get("user/remove/admin/" + userIdToRemove + "/group/" + groupUrl) - } - - public grantUserAdminInConversation (userIdToGrant: number | string, groupId: string): Promise { - return this.instance.get("user/grant/" + userIdToGrant + "/group/" + groupId) - } - - public uploadFile (data: FormData): Promise { - return this.instance.post("upload", data) - } +import axios, {AxiosResponse} from "axios" +import {GroupModel} from "../interface-contract/group-model" +import {IUserWrapper} from "../interface-contract/user/user-wrapper" +import {JwtModel} from "../interface-contract/jwt-model" +import {IUser} from "../interface-contract/user/user-model" +import {GroupUserModel} from "../interface-contract/group-user-model" +import {HttpMainService} from "./http-main.service" +import {Csrf} from "../interface-contract/csrf/csrf.type" + +export class HttpService extends HttpMainService { + + public constructor() { + super() + } + + public getCsrfToken(): Promise> { + return this.instance.get("csrf") + } + + public authenticate(jwtModel: JwtModel): Promise> { + return this.instance.post("auth", jwtModel) + } + + public async pingRoute(): Promise> { + return this.instance.get("fetch") + } + + public async ensureRoomExists(roomId: string): Promise> { + return await axios.get(`http://localhost:9090/room/ensure-room-exists/${roomId}`, {withCredentials: true}) + } + + public logout(): Promise { + return this.instance.get("logout") + } + + public createGroup(groupName: string): Promise> { + return this.instance.post("create", {name: groupName}) + } + + public addUserToGroup(userId: number | string, groupUrl: string): Promise { + return this.instance.get("user/add/" + userId + "/" + groupUrl) + } + + public fetchAllUsersInConversation(groupUrl: string): Promise> { + return this.instance.get("users/group/" + groupUrl, {}) + } + + public fetchAllUsersWithoutAlreadyInGroup(groupUrl: string): Promise> { + return this.instance.get("users/all/" + groupUrl, {}) + } + + createUser(firstname: string, lastname: string, email: string, password: string): Promise { + return this.instance.post("user/register", { + firstname, + lastname, + email, + password + }) + } + + public leaveConversation(userIdToRemove: number, groupId: string): Promise { + return this.instance.get("user/leave/" + userIdToRemove + "/group/" + groupId) + } + + public removeUserFromConversation(userIdToRemove: string | number, groupUrl: string): Promise { + return this.instance.get("user/remove/" + userIdToRemove + "/group/" + groupUrl) + } + + public removeAdminUserInConversation(userIdToRemove: string | number, groupUrl: string): Promise { + return this.instance.get("user/remove/admin/" + userIdToRemove + "/group/" + groupUrl) + } + + public grantUserAdminInConversation(userIdToGrant: number | string, groupId: string): Promise { + return this.instance.get("user/grant/" + userIdToGrant + "/group/" + groupId) + } + + public uploadFile(data: FormData): Promise { + return this.instance.post("upload", data) + } } diff --git a/frontend-web/src/utils/string-size-calculator.ts b/frontend-web/src/utils/string-size-calculator.ts index 3b22892..b946e20 100644 --- a/frontend-web/src/utils/string-size-calculator.ts +++ b/frontend-web/src/utils/string-size-calculator.ts @@ -1,3 +1,3 @@ export const getPayloadSize = (value: string): number => { - return Buffer.from(value).length + return new Blob([value]).size }