From 98d5b073270a21a6a3e6df79dd819ae50f26ff79 Mon Sep 17 00:00:00 2001 From: dominikkovacevic Date: Fri, 24 May 2024 11:09:11 +0200 Subject: [PATCH 1/7] Initial commit for websocket image and audio uploading functionality. Still not finished and needs to be tested --- .../RimatchBackendApplication.java | 2 +- .../controller/MessageController.java | 67 +++++++++++++++++-- .../controller/UserController.java | 12 ++-- .../controller/WebSocketController.java | 7 +- .../rimatchbackend/dto/MessageDTO.java | 11 ++- .../rimatch/rimatchbackend/model/Match.java | 2 +- .../rimatch/rimatchbackend/model/Message.java | 6 +- .../rimatchbackend/model/MessageType.java | 8 +++ .../rimatchbackend/service/S3Service.java | 25 ++++++- 9 files changed, 118 insertions(+), 22 deletions(-) create mode 100644 backend/src/main/java/com/rimatch/rimatchbackend/model/MessageType.java diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/RimatchBackendApplication.java b/backend/src/main/java/com/rimatch/rimatchbackend/RimatchBackendApplication.java index be0971d..96804e5 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/RimatchBackendApplication.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/RimatchBackendApplication.java @@ -26,7 +26,7 @@ class MyController { @GetMapping("/") public Map getMessage() { Map response = new HashMap<>(); - response.put("message", "Different message"); + response.put("message", "Rimatch backend running!"); return response; } } \ No newline at end of file diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/MessageController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MessageController.java index 7374c85..a08d2c0 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/MessageController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MessageController.java @@ -1,13 +1,21 @@ package com.rimatch.rimatchbackend.controller; +import com.rimatch.rimatchbackend.model.Match; import com.rimatch.rimatchbackend.model.Message; +import com.rimatch.rimatchbackend.model.User; +import com.rimatch.rimatchbackend.repository.MatchRepository; import com.rimatch.rimatchbackend.repository.MessageRepository; +import com.rimatch.rimatchbackend.service.S3Service; +import com.rimatch.rimatchbackend.service.UserService; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.util.Optional; @@ -15,16 +23,52 @@ @RequestMapping("api/messages") public class MessageController { - @Autowired MessageRepository messageRepository; + @Autowired private MessageRepository messageRepository; - @Autowired - private MongoTemplate mongoTemplate; + @Autowired private MatchRepository matchRepository; + + @Autowired private UserService userService; + + @Autowired private S3Service s3Service; + + @PostMapping("/upload-image") + public ResponseEntity uploadImage(@RequestBody MultipartFile photo,String chatId,HttpServletRequest request) throws Exception { + String authToken = request.getHeader("Authorization"); + User user = userService.getUserByToken(authToken); + System.out.println(chatId); + Optional match = matchRepository.findById(chatId); + if(match.isPresent()){ + System.out.println(match.get().getFirstUserId()); + System.out.println(match.get().getSecondUserId()); + System.out.println(authToken); + if(!userHasAccessToChat(match.get(),user.getId())){ + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + }else{ + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + return ResponseEntity.ok(s3Service.uploadImage(photo,chatId,"chats")); + } + + /*@PostMapping("/upload-voice") + public ResponseEntity uploadVoice(@RequestBody MultipartFile audio,String chatId,HttpServletRequest request) throws Exception { + String authToken = request.getHeader("Authorization"); + Optional message = messageRepository.findById(chatId); + if(message.isPresent()){ + if(!userHasAccessToChat(message.get(),authToken)){ + ResponseEntity.status(400); + } + } + return ResponseEntity.ok(s3Service.uploadAudioFile(audio,chatId,"chats")); + }*/ @GetMapping("/{chatId}") public Page getMessagesFromId(@PathVariable String chatId, @RequestParam(required = false) Integer page, @RequestParam(required = false) Integer pageSize, - @RequestParam(required = false) String messageId) { + @RequestParam(required = false) String messageId, + HttpServletRequest request) { + String authToken = request.getHeader("Authorization"); if(page == null) page = 0; if(pageSize == null) pageSize = 5; Pageable p = PageRequest.of(page,pageSize); @@ -32,7 +76,7 @@ public Page getMessagesFromId(@PathVariable String chatId, Optional m = messageRepository.findById(messageId); if(m.isPresent()){ - + //if(!userHasAccessToChat(m.get(),authToken)) return Page.empty(); //should be handled better but due to time left like this return messageRepository.findByChatIdAndTimestampBeforeOrderByTimestampDesc(chatId,m.get().getTimestamp(), p); }else{ return null; @@ -40,4 +84,15 @@ public Page getMessagesFromId(@PathVariable String chatId, } return messageRepository.findByChatIdOrderByTimestampDesc(chatId,p); } + + private boolean userHasAccessToChat(Match match,String userId){ + if(match.getFirstUserId().equals(userId) || match.getSecondUserId().equals(userId)){ + if(match.isAccepted() && match.isFinished()){ + return true; + } + return false; + } + return false; + } + } \ No newline at end of file diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java index 4f89fff..f948c0b 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java @@ -81,7 +81,7 @@ public ResponseEntity setupUser(@Valid @RequestPart("data") SetupDto setupDto if(setupDto.getPreferences().getAgeGroupMax() < setupDto.getPreferences().getAgeGroupMin()){ return ResponseEntity.badRequest().body(createErrorMap("ageGroupMax must be higher or equal to ageGroupMin")); } - String url = s3Service.uploadFile(file); + String url = s3Service.uploadImage(file,user.getId(),"users"); setupDto.setProfileImageUrl(url); user = userService.finishUserSetup(user,setupDto); return ResponseEntity.ok(user); @@ -107,7 +107,7 @@ public ResponseEntity updatePreferences(@Valid @RequestBody PreferencesUpdate return new ResponseEntity<>(HttpStatus.OK); } - @PostMapping("/me/profilePicture") + @PostMapping("/me/profile-picture") public ResponseEntity changeProfilePicure(@RequestBody MultipartFile photo,HttpServletRequest request){ if (!photo.getContentType().startsWith("image/")) { return ResponseEntity.badRequest().body(createErrorMap("File "+photo.getOriginalFilename()+" is not an image so it can't be used!")); @@ -116,7 +116,7 @@ public ResponseEntity changeProfilePicure(@RequestBody MultipartFile photo,Ht User user = userService.getUserByToken(authToken); String newProfileImageUrl; try { - newProfileImageUrl = s3Service.uploadFile(photo); + newProfileImageUrl = s3Service.uploadImage(photo,user.getId(),"users"); } catch (Exception e) { throw new RuntimeException(e); } @@ -130,7 +130,7 @@ public ResponseEntity changeProfilePicure(@RequestBody MultipartFile photo,Ht return new ResponseEntity<>(HttpStatus.OK); } - @PostMapping("/me/addPhotos") + @PostMapping("/me/add-photos") public ResponseEntity uploadPhotos(@RequestBody List photos,HttpServletRequest request) throws Exception { for (MultipartFile photo : photos) { if (!photo.getContentType().startsWith("image/")) { @@ -141,14 +141,14 @@ public ResponseEntity uploadPhotos(@RequestBody List photos,Ht User user = userService.getUserByToken(authToken); List urls = new ArrayList<>(); for (MultipartFile photo : photos) { - String url = s3Service.uploadFile(photo); + String url = s3Service.uploadImage(photo,user.getId(),"users"); urls.add(url); } userService.addPhotos(user,urls); return new ResponseEntity<>(HttpStatus.OK); } - @PostMapping("/me/removePhotos") + @PostMapping("/me/remove-photos") public ResponseEntity removePhotos(@RequestBody List urls, HttpServletRequest request){ String authToken = request.getHeader("Authorization"); User user = userService.getUserByToken(authToken); diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/WebSocketController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/WebSocketController.java index b412cae..dfef494 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/WebSocketController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/WebSocketController.java @@ -3,6 +3,7 @@ import com.rimatch.rimatchbackend.dto.MessageDTO; import com.rimatch.rimatchbackend.model.Match; import com.rimatch.rimatchbackend.model.Message; +import com.rimatch.rimatchbackend.model.MessageType; import com.rimatch.rimatchbackend.model.User; import com.rimatch.rimatchbackend.repository.MatchRepository; import com.rimatch.rimatchbackend.repository.MessageRepository; @@ -41,7 +42,7 @@ public class WebSocketController { @Autowired private MatchRepository matchRepository; - @MessageMapping("/sendMessage") + @MessageMapping("/sendMessage")//todo:CHANGE BEFORE PUSHING public void sendMessage(@Payload MessageDTO messageDTO, @Header("Authorization") String token) { try { User sender = userService.getUserByToken(token.substring(7)); @@ -53,8 +54,8 @@ public void sendMessage(@Payload MessageDTO messageDTO, @Header("Authorization") if(m.get().isFinished() && !m.get().isAccepted()) return; //"blocked" String receiverId = messageDTO.getReceiverId(); - Message message = new Message(messageDTO.getChatId(),sender.getId(),receiverId); - message.setContent(messageDTO.getContent()); + Message message = new Message(messageDTO.getChatId(),sender.getId(),receiverId,messageDTO.getMessageType()); + message.setContent(messageDTO.getTextContent()); messageRepository.save(message); messagingTemplate.convertAndSend(receiverId+"/queue/messages", message); } catch (JwtException | IllegalArgumentException ex) { diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/dto/MessageDTO.java b/backend/src/main/java/com/rimatch/rimatchbackend/dto/MessageDTO.java index 7f8f80d..193fea3 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/dto/MessageDTO.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/dto/MessageDTO.java @@ -1,5 +1,6 @@ package com.rimatch.rimatchbackend.dto; +import com.rimatch.rimatchbackend.model.MessageType; import jakarta.validation.constraints.NotBlank; import lombok.Data; @@ -12,5 +13,13 @@ public class MessageDTO { private String chatId; @NotBlank - private String content; + private MessageType messageType; + + private String textContent; + + private String imageContentUrl; + + private String voiceContentUrl; + + private String replyId; } \ No newline at end of file diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/model/Match.java b/backend/src/main/java/com/rimatch/rimatchbackend/model/Match.java index bbc7bd5..6da11c9 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/model/Match.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/model/Match.java @@ -24,4 +24,4 @@ public class Match { private boolean accepted = false; private boolean finished = false; -} +} \ No newline at end of file diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/model/Message.java b/backend/src/main/java/com/rimatch/rimatchbackend/model/Message.java index 306be7f..4ab8306 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/model/Message.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/model/Message.java @@ -24,8 +24,12 @@ public class Message { @NotNull private String receiverId; + @NotNull + private MessageType messageType; + @NotBlank private String content; private Date timestamp = new Date(); -} \ No newline at end of file +} + diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/model/MessageType.java b/backend/src/main/java/com/rimatch/rimatchbackend/model/MessageType.java new file mode 100644 index 0000000..964bd90 --- /dev/null +++ b/backend/src/main/java/com/rimatch/rimatchbackend/model/MessageType.java @@ -0,0 +1,8 @@ +package com.rimatch.rimatchbackend.model; + +public enum MessageType { + TEXT, + IMAGE, + VOICE, + REPLY +} diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/service/S3Service.java b/backend/src/main/java/com/rimatch/rimatchbackend/service/S3Service.java index e362192..5901754 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/service/S3Service.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/service/S3Service.java @@ -17,6 +17,7 @@ import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Date; @Service @@ -31,13 +32,31 @@ public S3Service(S3Client s3Client) { this.s3Client = s3Client; } - public String uploadFile(MultipartFile file) throws Exception { + public String uploadImage(MultipartFile file,String chatOrUserId,String folder) throws Exception { + String extension = file.getContentType(); + if (Arrays.asList("image/jpg", "image/jpeg", "image/png").contains(extension)) { + return uploadFile(file,"/images/",folder+'/'+chatOrUserId); + }else{ + throw new Exception("File must be jpg,jpeg or png"); + } + } + + public String uploadAudioFile(MultipartFile file,String chatId,String folder) throws Exception { + String extension = file.getContentType(); + if (Arrays.asList("audio/wav", "audio/mpeg").contains(extension)) { + return uploadFile(file,"/audio/",folder+'/'+chatId); + }else{ + throw new Exception("File must be jpg,jpeg or png"); + } + } + + public String uploadFile(MultipartFile file,String folder,String chatorUserId) throws Exception { String fileName = generateFileName(file); try { PutObjectRequest putObjectRequest = PutObjectRequest.builder() .bucket(bucketName) - .key(fileName) + .key(chatorUserId+folder+fileName) .acl(ObjectCannedACL.PUBLIC_READ) .build(); @@ -47,7 +66,7 @@ public String uploadFile(MultipartFile file) throws Exception { } GetUrlRequest getUrlRequest = GetUrlRequest.builder() .bucket(bucketName) - .key(fileName) + .key(chatorUserId+folder+fileName) .build(); return s3Client.utilities().getUrl(getUrlRequest).toString(); From 8ea8ffa1929ad166a3ed345bb4f0f70f334e0e11 Mon Sep 17 00:00:00 2001 From: dominikkovacevic Date: Fri, 31 May 2024 14:45:48 +0200 Subject: [PATCH 2/7] Finished websocket for sending audio and images to chat. --- .../controller/WebSocketController.java | 12 ++++++++++-- .../com/rimatch/rimatchbackend/dto/MessageDTO.java | 7 ++----- .../com/rimatch/rimatchbackend/model/Message.java | 4 +++- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/WebSocketController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/WebSocketController.java index dfef494..ccd77d6 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/WebSocketController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/WebSocketController.java @@ -10,6 +10,8 @@ import com.rimatch.rimatchbackend.service.UserService; import com.rimatch.rimatchbackend.util.JWTUtils; import io.jsonwebtoken.JwtException; +import jakarta.validation.constraints.Null; +import org.apache.commons.lang3.ObjectUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -54,8 +56,14 @@ public void sendMessage(@Payload MessageDTO messageDTO, @Header("Authorization") if(m.get().isFinished() && !m.get().isAccepted()) return; //"blocked" String receiverId = messageDTO.getReceiverId(); - Message message = new Message(messageDTO.getChatId(),sender.getId(),receiverId,messageDTO.getMessageType()); - message.setContent(messageDTO.getTextContent()); + Message message = new Message(messageDTO.getChatId(),sender.getId(),receiverId,messageDTO.getMessageType(),messageDTO.getContent()); + if(messageDTO.getMessageType() == MessageType.REPLY){ + if(!messageDTO.getReplyId().isEmpty()){ + message.setReplyId(message.getReplyId()); + }else{ + return; + } + } messageRepository.save(message); messagingTemplate.convertAndSend(receiverId+"/queue/messages", message); } catch (JwtException | IllegalArgumentException ex) { diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/dto/MessageDTO.java b/backend/src/main/java/com/rimatch/rimatchbackend/dto/MessageDTO.java index 193fea3..5aaf01c 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/dto/MessageDTO.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/dto/MessageDTO.java @@ -15,11 +15,8 @@ public class MessageDTO { @NotBlank private MessageType messageType; - private String textContent; - - private String imageContentUrl; - - private String voiceContentUrl; + @NotBlank + private String content; private String replyId; } \ No newline at end of file diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/model/Message.java b/backend/src/main/java/com/rimatch/rimatchbackend/model/Message.java index 4ab8306..8fa0938 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/model/Message.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/model/Message.java @@ -27,9 +27,11 @@ public class Message { @NotNull private MessageType messageType; - @NotBlank + @NotNull private String content; + private String replyId; + private Date timestamp = new Date(); } From a0a85be0f583142f86dc07a559fc1726fe01efe9 Mon Sep 17 00:00:00 2001 From: dominikkovacevic Date: Sun, 2 Jun 2024 21:02:52 +0200 Subject: [PATCH 3/7] Removed some debugging statements from websocket code. --- .../rimatchbackend/controller/MessageController.java | 4 ---- .../rimatch/rimatchbackend/controller/UserController.java | 7 +++---- .../rimatchbackend/controller/WebSocketController.java | 2 +- .../java/com/rimatch/rimatchbackend/service/S3Service.java | 2 +- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/MessageController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MessageController.java index a08d2c0..dcc7840 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/MessageController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MessageController.java @@ -35,12 +35,8 @@ public class MessageController { public ResponseEntity uploadImage(@RequestBody MultipartFile photo,String chatId,HttpServletRequest request) throws Exception { String authToken = request.getHeader("Authorization"); User user = userService.getUserByToken(authToken); - System.out.println(chatId); Optional match = matchRepository.findById(chatId); if(match.isPresent()){ - System.out.println(match.get().getFirstUserId()); - System.out.println(match.get().getSecondUserId()); - System.out.println(authToken); if(!userHasAccessToChat(match.get(),user.getId())){ return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); } diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java index f948c0b..8cd9040 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/UserController.java @@ -1,8 +1,6 @@ package com.rimatch.rimatchbackend.controller; -import com.rimatch.rimatchbackend.dto.PreferencesUpdateDTO; -import com.rimatch.rimatchbackend.dto.SetupDto; -import com.rimatch.rimatchbackend.dto.UserUpdateDTO; +import com.rimatch.rimatchbackend.dto.*; import com.rimatch.rimatchbackend.model.User; import com.rimatch.rimatchbackend.service.S3Service; import com.rimatch.rimatchbackend.service.UserService; @@ -70,9 +68,10 @@ public ResponseEntity getUser(HttpServletRequest request) { } @PostMapping("/me/setup") - public ResponseEntity setupUser(@Valid @RequestPart("data") SetupDto setupDto, @RequestPart("photo") MultipartFile file, HttpServletRequest request) throws Exception { + public ResponseEntity setupUser(@RequestParam("photo") MultipartFile file,@Valid @RequestPart("data") SetupDto setupDto, HttpServletRequest request) throws Exception { String authToken = request.getHeader("Authorization"); User user = userService.getUserByToken(authToken); + System.out.println(file.getContentType()); if(user.isActive()){ Map map = new HashMap<>(); map.put("message","Setup was already done!"); diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/WebSocketController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/WebSocketController.java index ccd77d6..5427e79 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/WebSocketController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/WebSocketController.java @@ -44,7 +44,7 @@ public class WebSocketController { @Autowired private MatchRepository matchRepository; - @MessageMapping("/sendMessage")//todo:CHANGE BEFORE PUSHING + @MessageMapping("/send-message") public void sendMessage(@Payload MessageDTO messageDTO, @Header("Authorization") String token) { try { User sender = userService.getUserByToken(token.substring(7)); diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/service/S3Service.java b/backend/src/main/java/com/rimatch/rimatchbackend/service/S3Service.java index 5901754..e2d8264 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/service/S3Service.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/service/S3Service.java @@ -37,7 +37,7 @@ public String uploadImage(MultipartFile file,String chatOrUserId,String folder) if (Arrays.asList("image/jpg", "image/jpeg", "image/png").contains(extension)) { return uploadFile(file,"/images/",folder+'/'+chatOrUserId); }else{ - throw new Exception("File must be jpg,jpeg or png"); + throw new Exception("File must be jpg, jpeg or png"); } } From f2678de240922bbe5351dec0b0aa618d6268ebe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Thu, 6 Jun 2024 13:12:12 +0200 Subject: [PATCH 4/7] Updating api endpoint changes. --- frontend/src/api/messages.ts | 53 ++++++++++++++++-------------------- frontend/src/api/users.ts | 6 ++-- mobileV2/src/api/messages.ts | 53 ++++++++++++++++-------------------- mobileV2/src/api/users.ts | 6 ++-- 4 files changed, 54 insertions(+), 64 deletions(-) diff --git a/frontend/src/api/messages.ts b/frontend/src/api/messages.ts index 5044132..bd956be 100644 --- a/frontend/src/api/messages.ts +++ b/frontend/src/api/messages.ts @@ -1,14 +1,10 @@ import useAxiosPrivate from "@/hooks/useAxiosPrivate"; -import { Message } from "@/types/Message"; -import { - useInfiniteQuery, - useQuery, - useQueryClient, -} from "@tanstack/react-query"; -import { useStompClient, useSubscription } from "react-stomp-hooks"; -import { Page } from "@/types/Page"; +import {Message} from "@/types/Message"; +import {useInfiniteQuery, useQuery, useQueryClient,} from "@tanstack/react-query"; +import {useStompClient, useSubscription} from "react-stomp-hooks"; +import {Page} from "@/types/Page"; import useAuth from "@/hooks/useAuth"; -import { MatchedUser } from "@/types/User"; +import {MatchedUser} from "@/types/User"; export const HISTORY_PAGE_SIZE = 20; export const MESSAGE_PAGE_SIZE = 15; @@ -19,39 +15,38 @@ export const MessagesService = { const queryClient = useQueryClient(); const { auth } = useAuth(); - const sendMessage = ( - content: string, - receiverId: string, - chatId: string + return ( + content: string, + receiverId: string, + chatId: string ) => { if (!client) { throw new Error("Stomp client not initialized"); } client.publish({ - destination: `/app/sendMessage`, - body: JSON.stringify({ content, receiverId, chatId }), + destination: `/app/send-message`, + body: JSON.stringify({content, receiverId, chatId}), headers: { Authorization: `Bearer ${auth?.accessToken}`, }, }); queryClient.setQueryData( - ["messages", chatId], - (oldData: Page) => { - const newContent = [...oldData.content]; - newContent.unshift({ - id: new Date().getTime().toString() + content, - content, - senderId: "", - receiverId, - chatId, - timestamp: new Date().toISOString(), - }); - return { ...oldData, content: newContent }; - } + ["messages", chatId], + (oldData: Page) => { + const newContent = [...oldData.content]; + newContent.unshift({ + id: new Date().getTime().toString() + content, + content, + senderId: "", + receiverId, + chatId, + timestamp: new Date().toISOString(), + }); + return {...oldData, content: newContent}; + } ); }; - return sendMessage; }, useGetMessages: (chatId?: string) => { diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index 60328e8..ecd9aea 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -94,7 +94,7 @@ export const UsersService = { const form = new FormData(); form.append("photo", file); const response = await axios.postForm( - "/users/me/profilePicture", + "/users/me/profile-picture", form ); return response.data; @@ -120,7 +120,7 @@ export const UsersService = { mutationFn: async (files) => { const form = new FormData(); files.forEach((file) => form.append("photos", file)); - const response = await axios.postForm("/users/me/addPhotos", form); + const response = await axios.postForm("/users/me/add-photos", form); return response.data; }, onSuccess: () => { @@ -142,7 +142,7 @@ export const UsersService = { const queryClient = useQueryClient(); return useMutation>({ mutationFn: async (urls) => { - const response = await axios.post("/users/me/removePhotos", urls); + const response = await axios.post("/users/me/remove-photos", urls); return response.data; }, onSuccess: () => { diff --git a/mobileV2/src/api/messages.ts b/mobileV2/src/api/messages.ts index a41df52..b915aad 100644 --- a/mobileV2/src/api/messages.ts +++ b/mobileV2/src/api/messages.ts @@ -1,14 +1,10 @@ import useAxiosPrivate from "@/hooks/useAxiosPrivate"; -import { - useInfiniteQuery, - useQuery, - useQueryClient, -} from "@tanstack/react-query"; -import { useStompClient, useSubscription } from "react-stomp-hooks"; +import {useInfiniteQuery, useQuery, useQueryClient,} from "@tanstack/react-query"; +import {useStompClient, useSubscription} from "react-stomp-hooks"; import useAuth from "@/hooks/useAuth"; -import { MatchedUser } from "@/types/User"; -import { Message } from "@/types/Message"; -import { Page } from "@/types/Page"; +import {MatchedUser} from "@/types/User"; +import {Message} from "@/types/Message"; +import {Page} from "@/types/Page"; import useCurrentUserContext from "@/hooks/useCurrentUser"; export const HISTORY_PAGE_SIZE = 15; @@ -21,39 +17,38 @@ export const MessagesService = { const { auth } = useAuth(); const currentUser = useCurrentUserContext(); - const sendMessage = ( - content: string, - receiverId: string, - chatId: string + return ( + content: string, + receiverId: string, + chatId: string ) => { if (!client) { throw new Error("Stomp client not initialized"); } client.publish({ - destination: `/app/sendMessage`, - body: JSON.stringify({ content, receiverId, chatId }), + destination: `/app/send-message`, + body: JSON.stringify({content, receiverId, chatId}), headers: { Authorization: `Bearer ${auth?.accessToken}`, }, }); queryClient.setQueryData( - ["messages", chatId], - (oldData: Page) => { - const newContent = [...oldData.content]; - newContent.unshift({ - id: new Date().getTime().toString() + content, - content, - senderId: currentUser.id, - receiverId, - chatId, - timestamp: new Date().toISOString(), - }); - return { ...oldData, content: newContent }; - } + ["messages", chatId], + (oldData: Page) => { + const newContent = [...oldData.content]; + newContent.unshift({ + id: new Date().getTime().toString() + content, + content, + senderId: currentUser.id, + receiverId, + chatId, + timestamp: new Date().toISOString(), + }); + return {...oldData, content: newContent}; + } ); }; - return sendMessage; }, useGetMessages: (chatId?: string) => { diff --git a/mobileV2/src/api/users.ts b/mobileV2/src/api/users.ts index 1505b3d..4ede430 100644 --- a/mobileV2/src/api/users.ts +++ b/mobileV2/src/api/users.ts @@ -94,7 +94,7 @@ export const UsersService = { const form = new FormData(); form.append("photo", file); const response = await axios.postForm( - "/users/me/profilePicture", + "/users/me/profile-picture", form ); return response.data; @@ -120,7 +120,7 @@ export const UsersService = { mutationFn: async (files) => { const form = new FormData(); files.forEach((file) => form.append("photos", file)); - const response = await axios.postForm("/users/me/addPhotos", form); + const response = await axios.postForm("/users/me/add-photos", form); return response.data; }, onSuccess: () => { @@ -142,7 +142,7 @@ export const UsersService = { const queryClient = useQueryClient(); return useMutation>({ mutationFn: async (urls) => { - const response = await axios.post("/users/me/removePhotos", urls); + const response = await axios.post("/users/me/remove-photos", urls); return response.data; }, onSuccess: () => { From e0dceecf975325e156783259093f135adeeb6328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Thu, 6 Jun 2024 15:19:55 +0200 Subject: [PATCH 5/7] Chat image upload design. --- .editorconfig | 9 ++ frontend/frontend.iml | 9 ++ frontend/src/api/messages.ts | 75 +++++++++------ frontend/src/components/chat/ChatInput.tsx | 93 ++++++++++++++----- .../src/types/{Message.d.ts => Message.ts} | 12 +++ frontend/src/views/ChatPage.tsx | 77 ++++++++------- mobileV2/mobileV2.iml | 9 ++ 7 files changed, 196 insertions(+), 88 deletions(-) create mode 100644 .editorconfig create mode 100644 frontend/frontend.iml rename frontend/src/types/{Message.d.ts => Message.ts} (50%) create mode 100644 mobileV2/mobileV2.iml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9acfa30 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +indent_size = 2 diff --git a/frontend/frontend.iml b/frontend/frontend.iml new file mode 100644 index 0000000..8021953 --- /dev/null +++ b/frontend/frontend.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/api/messages.ts b/frontend/src/api/messages.ts index bd956be..6f6b146 100644 --- a/frontend/src/api/messages.ts +++ b/frontend/src/api/messages.ts @@ -1,6 +1,6 @@ import useAxiosPrivate from "@/hooks/useAxiosPrivate"; -import {Message} from "@/types/Message"; -import {useInfiniteQuery, useQuery, useQueryClient,} from "@tanstack/react-query"; +import {Message, MessageImageUploadData} from "@/types/Message"; +import {useInfiniteQuery, useMutation, UseMutationOptions, useQuery, useQueryClient,} from "@tanstack/react-query"; import {useStompClient, useSubscription} from "react-stomp-hooks"; import {Page} from "@/types/Page"; import useAuth from "@/hooks/useAuth"; @@ -13,12 +13,12 @@ export const MessagesService = { useSendMessage: () => { const client = useStompClient(); const queryClient = useQueryClient(); - const { auth } = useAuth(); + const {auth} = useAuth(); return ( - content: string, - receiverId: string, - chatId: string + content: string, + receiverId: string, + chatId: string ) => { if (!client) { throw new Error("Stomp client not initialized"); @@ -32,19 +32,19 @@ export const MessagesService = { }, }); queryClient.setQueryData( - ["messages", chatId], - (oldData: Page) => { - const newContent = [...oldData.content]; - newContent.unshift({ - id: new Date().getTime().toString() + content, - content, - senderId: "", - receiverId, - chatId, - timestamp: new Date().toISOString(), - }); - return {...oldData, content: newContent}; - } + ["messages", chatId], + (oldData: Page) => { + const newContent = [...oldData.content]; + newContent.unshift({ + id: new Date().getTime().toString() + content, + content, + senderId: "", + receiverId, + chatId, + timestamp: new Date().toISOString(), + }); + return {...oldData, content: newContent}; + } ); }; }, @@ -67,7 +67,7 @@ export const MessagesService = { useGetMessagesHistory: (chatId: string, lastMessageId: string) => { const axios = useAxiosPrivate(); - const fetchMessages = async ({ pageParam }: { pageParam: unknown }) => { + const fetchMessages = async ({pageParam}: { pageParam: unknown }) => { const res = await axios.get>( `/messages/${chatId}?page=${pageParam}&messageId=${lastMessageId}&pageSize=${HISTORY_PAGE_SIZE}` ); @@ -93,7 +93,7 @@ export const MessagesService = { (oldData: Page) => { const newContent = [...oldData.content]; newContent.unshift(newMessage); - return { ...oldData, content: newContent }; + return {...oldData, content: newContent}; } ); queryClient.setQueryData( @@ -101,16 +101,33 @@ export const MessagesService = { (oldData: Array) => oldData ? [...oldData].sort((a, b) => { - if (a.chatId === newMessage.chatId) { - return -1; - } - if (b.chatId === newMessage.chatId) { - return 1; - } - return 0; - }) + if (a.chatId === newMessage.chatId) { + return -1; + } + if (b.chatId === newMessage.chatId) { + return 1; + } + return 0; + }) : oldData ); }); }, + + useUploadImage: ( + options?: Omit, "mutationFn"> + ) => { + const axios = useAxiosPrivate(); + + return useMutation({ + mutationFn: async ({chatId, photo}) => { + const formData = new FormData(); + formData.append("photo", photo); + formData.append("chatId", chatId); + const {data} = await axios.postForm("/messages/upload-image", formData); + return data; + }, + ...options, + }); + } }; diff --git a/frontend/src/components/chat/ChatInput.tsx b/frontend/src/components/chat/ChatInput.tsx index 4fe1965..9ceaab6 100644 --- a/frontend/src/components/chat/ChatInput.tsx +++ b/frontend/src/components/chat/ChatInput.tsx @@ -1,19 +1,28 @@ -import { Field, Form, Formik, FormikHelpers, FormikValues } from "formik"; +import {Field, Form, Formik, FormikHelpers, useField} from "formik"; import * as Yup from "yup"; import SendIcon from "@mui/icons-material/Send"; +import PhotoCameraIcon from "@mui/icons-material/PhotoCamera"; +import CloseIcon from '@mui/icons-material/Close'; +import {useMemo} from "react"; + +export interface ChatInputValues { + message: string; + image: File | null; +} + const chatValidation = Yup.object({ - message: Yup.string().required(), + message: Yup.string(), }); -interface ChatInputProps { - handleSubmit: (values: T, helpers: FormikHelpers) => void; - initialValues: T; +interface ChatInputProps { + handleSubmit: (values: ChatInputValues, helpers: FormikHelpers) => void; + initialValues: ChatInputValues; } -const ChatInput = ({ - handleSubmit, - initialValues, -}: ChatInputProps) => { +const ChatInput = ({ + handleSubmit, + initialValues, + }: ChatInputProps) => { const submitOnEnter = ( e: React.KeyboardEvent, submitForm: () => Promise @@ -29,29 +38,65 @@ const ChatInput = ({ validationSchema={chatValidation} onSubmit={handleSubmit} > - {({ submitForm }) => ( + {({submitForm, setFieldValue}) => (
- ) => - submitOnEnter(e, submitForm) - } - /> - +
+ setFieldValue("image", null)}/> +
+ ) => + submitOnEnter(e, submitForm) + } + /> + + +
+
)} ); }; +const ChatImageDisplay = ({onRemove}: { onRemove: () => void }) => { + const [field] = useField("image"); + const imageUrl = useMemo(() => { + if (!field.value) return null; + return URL.createObjectURL(field.value); + }, [field.value]); + + if (!imageUrl) return null; + return ( +
+
+ Chat image + +
+
+ ); +} + export default ChatInput; diff --git a/frontend/src/types/Message.d.ts b/frontend/src/types/Message.ts similarity index 50% rename from frontend/src/types/Message.d.ts rename to frontend/src/types/Message.ts index 491b6d9..3e7d014 100644 --- a/frontend/src/types/Message.d.ts +++ b/frontend/src/types/Message.ts @@ -6,3 +6,15 @@ export interface Message { content: string; timestamp: string; } + +export enum MessageType { + TEXT, + IMAGE, + VOICE, + REPLY +} + +export interface MessageImageUploadData { + chatId: string; + photo: File; +} diff --git a/frontend/src/views/ChatPage.tsx b/frontend/src/views/ChatPage.tsx index 50dfe59..11fd120 100644 --- a/frontend/src/views/ChatPage.tsx +++ b/frontend/src/views/ChatPage.tsx @@ -1,47 +1,47 @@ -import { MessagesService } from "@/api/messages"; +import {MessagesService} from "@/api/messages"; import UserAvatar from "@/components/UserAvatar"; -import { ProjectedUser } from "@/types/User"; -import { FormikHelpers } from "formik"; -import { useEffect, useRef } from "react"; -import { Link, Navigate, useParams } from "react-router-dom"; +import {ProjectedUser} from "@/types/User"; +import {FormikHelpers} from "formik"; +import {useEffect, useRef} from "react"; +import {Link, Navigate, useParams} from "react-router-dom"; import KeyboardArrowLeftIcon from "@mui/icons-material/KeyboardArrowLeft"; import React from "react"; import ChatMessage from "@/components/chat/ChatMessage"; import ChatHistory from "@/components/chat/ChatHistory"; -import { CircularProgress } from "@mui/material"; -import ChatInput from "@/components/chat/ChatInput"; -import { useInView } from "react-intersection-observer"; +import {CircularProgress} from "@mui/material"; +import ChatInput, {ChatInputValues} from "@/components/chat/ChatInput"; +import {useInView} from "react-intersection-observer"; import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; -import { useAutoAnimate } from "@formkit/auto-animate/react"; +import {useAutoAnimate} from "@formkit/auto-animate/react"; import UserActionsDropdown from "@/components/UserActionsDropdown"; -import { MatchesService } from "@/api/matches"; +import {MatchesService} from "@/api/matches"; const initialValues = { message: "", + image: null }; -type ChatValues = typeof initialValues; const ChatPage = () => { - const [messagesStartRef, messagesStartInView] = useInView({ delay: 500 }); - const [messagesEndRef, messagesEndInView] = useInView({ triggerOnce: true }); + const [messagesStartRef, messagesStartInView] = useInView({delay: 500}); + const [messagesEndRef, messagesEndInView] = useInView({triggerOnce: true}); const startRef = useRef(null); const [parent] = useAutoAnimate(); - const { userId } = useParams() as { userId: string }; + const {userId} = useParams() as { userId: string }; const userQuery = MatchesService.useGetMatchedUserById(userId); const recentMessages = MessagesService.useGetMessages(userQuery.data?.chatId); const sendMessage = MessagesService.useSendMessage(); useEffect(() => { if (!recentMessages.isSuccess) { - startRef?.current?.scrollIntoView({ behavior: "smooth" }); + startRef?.current?.scrollIntoView({behavior: "smooth"}); } }, [recentMessages.isSuccess]); useEffect(() => { if (messagesStartInView && recentMessages.isSuccess) { - startRef?.current?.scrollIntoView({ behavior: "smooth" }); + startRef?.current?.scrollIntoView({behavior: "smooth"}); } }, [ messagesStartInView, @@ -53,22 +53,24 @@ const ChatPage = () => { return (
- +
); } if (userQuery.isError || !userQuery.isSuccess) { - return ; + return ; } const user = userQuery.data; const handleSubmit = ( - values: ChatValues, - helpers: FormikHelpers + values: ChatInputValues, + helpers: FormikHelpers ) => { + console.log(values) + return; sendMessage(values.message, user.id, user.chatId); helpers.resetForm(); }; @@ -77,7 +79,7 @@ const ChatPage = () => { return (
- +
); @@ -93,22 +95,28 @@ const ChatPage = () => { )}
+
+ +
{recentMessages.data.content.map((message) => ( - + ))}
{!recentMessages.data.last && messagesEndInView && ( @@ -118,11 +126,8 @@ const ChatPage = () => { matchedUser={user} /> )} +
- - handleSubmit={handleSubmit} - initialValues={initialValues} - /> ); }; @@ -132,13 +137,15 @@ interface ChatPageHeaderProps { user?: ProjectedUser; } -const ChatPageHeader = ({ children, user }: ChatPageHeaderProps) => ( -
+const ChatPageHeader = ({children, user}: ChatPageHeaderProps) => ( +
{user && ( -
+
- + ( to={`/matches/profile/${user.id}`} className="flex items-center justify-center w-12 h-12 bg-gray-300 rounded-full" > - + - +
)} diff --git a/mobileV2/mobileV2.iml b/mobileV2/mobileV2.iml new file mode 100644 index 0000000..8021953 --- /dev/null +++ b/mobileV2/mobileV2.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file From c53fbe6c7bdad57cadb36fe5d54ece6a4a21408e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Fri, 7 Jun 2024 12:57:44 +0200 Subject: [PATCH 6/7] Handling different types of messages. --- frontend/src/api/{ => messages}/messages.ts | 97 +++++++++++++------ .../strategies/ImageMessageHandler.ts | 29 ++++++ .../api/messages/strategies/MessageHandler.ts | 18 ++++ .../messages/strategies/TextMessageHandler.ts | 15 +++ frontend/src/components/ChatListComponent.tsx | 23 ++++- .../src/components/ChatMessageRecived.tsx | 25 ----- frontend/src/components/ChatMessageSent.tsx | 23 ----- .../src/components/ScrollToFieldError.tsx | 40 -------- frontend/src/components/chat/ChatHistory.tsx | 2 +- frontend/src/components/chat/ChatInput.tsx | 60 +++++++----- frontend/src/components/chat/ChatMessage.tsx | 23 ++++- frontend/src/types/Message.ts | 31 +++++- frontend/src/views/ChatPage.tsx | 69 ++++++------- .../src/views/ListOfMatchesForChatPage.tsx | 2 +- frontend/src/views/chat/ChatLayout.tsx | 4 +- 15 files changed, 267 insertions(+), 194 deletions(-) rename frontend/src/api/{ => messages}/messages.ts (59%) create mode 100644 frontend/src/api/messages/strategies/ImageMessageHandler.ts create mode 100644 frontend/src/api/messages/strategies/MessageHandler.ts create mode 100644 frontend/src/api/messages/strategies/TextMessageHandler.ts delete mode 100644 frontend/src/components/ChatMessageRecived.tsx delete mode 100644 frontend/src/components/ChatMessageSent.tsx delete mode 100644 frontend/src/components/ScrollToFieldError.tsx diff --git a/frontend/src/api/messages.ts b/frontend/src/api/messages/messages.ts similarity index 59% rename from frontend/src/api/messages.ts rename to frontend/src/api/messages/messages.ts index 6f6b146..667e345 100644 --- a/frontend/src/api/messages.ts +++ b/frontend/src/api/messages/messages.ts @@ -1,32 +1,63 @@ -import useAxiosPrivate from "@/hooks/useAxiosPrivate"; -import {Message, MessageImageUploadData} from "@/types/Message"; -import {useInfiniteQuery, useMutation, UseMutationOptions, useQuery, useQueryClient,} from "@tanstack/react-query"; -import {useStompClient, useSubscription} from "react-stomp-hooks"; -import {Page} from "@/types/Page"; -import useAuth from "@/hooks/useAuth"; -import {MatchedUser} from "@/types/User"; +import useAxiosPrivate from "@/hooks/useAxiosPrivate.ts"; +import { + ChatInputValues, + Message, + MessageData, + MessageImageUploadData, + SUPPORTED_MESSAGE_TYPES, +} from "@/types/Message.ts"; +import { + useInfiniteQuery, + useMutation, + UseMutationOptions, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { useStompClient, useSubscription } from "react-stomp-hooks"; +import { Page } from "@/types/Page"; +import { MatchedUser } from "@/types/User"; +import useAuth from "@/hooks/useAuth.ts"; +import { MessageHandlerProvider } from "@api/messages/strategies/MessageHandler.ts"; export const HISTORY_PAGE_SIZE = 20; export const MESSAGE_PAGE_SIZE = 15; export const MessagesService = { + useSendChat: () => { + const sendMessage = MessagesService.useSendMessage(); + const handlers = SUPPORTED_MESSAGE_TYPES.map((type) => + MessageHandlerProvider[type].useProcessMessage() + ); + return async ( + chatData: ChatInputValues, + receiverId: string, + chatId: string + ) => { + for (const handler of handlers) { + const { content, messageType } = await handler( + chatData, + receiverId, + chatId + ); + sendMessage({ content, receiverId, chatId, messageType }); + } + }; + }, + useSendMessage: () => { const client = useStompClient(); const queryClient = useQueryClient(); - const {auth} = useAuth(); + const { auth } = useAuth(); - return ( - content: string, - receiverId: string, - chatId: string - ) => { + return ({ messageType, receiverId, chatId, content }: MessageData) => { + if (!content) return; if (!client) { throw new Error("Stomp client not initialized"); } client.publish({ destination: `/app/send-message`, - body: JSON.stringify({content, receiverId, chatId}), + body: JSON.stringify({ content, receiverId, chatId, messageType }), headers: { Authorization: `Bearer ${auth?.accessToken}`, }, @@ -41,9 +72,10 @@ export const MessagesService = { senderId: "", receiverId, chatId, + messageType, timestamp: new Date().toISOString(), }); - return {...oldData, content: newContent}; + return { ...oldData, content: newContent }; } ); }; @@ -67,7 +99,7 @@ export const MessagesService = { useGetMessagesHistory: (chatId: string, lastMessageId: string) => { const axios = useAxiosPrivate(); - const fetchMessages = async ({pageParam}: { pageParam: unknown }) => { + const fetchMessages = async ({ pageParam }: { pageParam: unknown }) => { const res = await axios.get>( `/messages/${chatId}?page=${pageParam}&messageId=${lastMessageId}&pageSize=${HISTORY_PAGE_SIZE}` ); @@ -93,7 +125,7 @@ export const MessagesService = { (oldData: Page) => { const newContent = [...oldData.content]; newContent.unshift(newMessage); - return {...oldData, content: newContent}; + return { ...oldData, content: newContent }; } ); queryClient.setQueryData( @@ -101,33 +133,40 @@ export const MessagesService = { (oldData: Array) => oldData ? [...oldData].sort((a, b) => { - if (a.chatId === newMessage.chatId) { - return -1; - } - if (b.chatId === newMessage.chatId) { - return 1; - } - return 0; - }) + if (a.chatId === newMessage.chatId) { + return -1; + } + if (b.chatId === newMessage.chatId) { + return 1; + } + return 0; + }) : oldData ); }); }, - useUploadImage: ( + useUploadImage: < + Response extends string, + Err extends Error, + Args extends MessageImageUploadData, + >( options?: Omit, "mutationFn"> ) => { const axios = useAxiosPrivate(); return useMutation({ - mutationFn: async ({chatId, photo}) => { + mutationFn: async ({ chatId, photo }) => { const formData = new FormData(); formData.append("photo", photo); formData.append("chatId", chatId); - const {data} = await axios.postForm("/messages/upload-image", formData); + const { data } = await axios.postForm( + "/messages/upload-image", + formData + ); return data; }, ...options, }); - } + }, }; diff --git a/frontend/src/api/messages/strategies/ImageMessageHandler.ts b/frontend/src/api/messages/strategies/ImageMessageHandler.ts new file mode 100644 index 0000000..bd100b6 --- /dev/null +++ b/frontend/src/api/messages/strategies/ImageMessageHandler.ts @@ -0,0 +1,29 @@ +import { MessageHandler } from "@api/messages/strategies/MessageHandler.ts"; +import { MessageType } from "@/types/Message.ts"; +import { MessagesService } from "@api/messages/messages.ts"; + +export const ImageMessageHandler: MessageHandler = { + getMessageType: function () { + return MessageType.IMAGE; + }, + useProcessMessage: function () { + const { mutateAsync } = MessagesService.useUploadImage(); + + return async (chatData, _receiverId, chatId) => { + const { image } = chatData; + + if (!image) { + return { + content: "", + messageType: this.getMessageType(), + }; + } + + const response = await mutateAsync({ chatId, photo: image }); + return { + content: response, + messageType: this.getMessageType(), + }; + }; + }, +}; diff --git a/frontend/src/api/messages/strategies/MessageHandler.ts b/frontend/src/api/messages/strategies/MessageHandler.ts new file mode 100644 index 0000000..cc71758 --- /dev/null +++ b/frontend/src/api/messages/strategies/MessageHandler.ts @@ -0,0 +1,18 @@ +import { ChatInputValues, MessageType } from "@/types/Message.ts"; +import { TextMessageHandler } from "@api/messages/strategies/TextMessageHandler.ts"; +import { ImageMessageHandler } from "@api/messages/strategies/ImageMessageHandler.ts"; + +export interface MessageHandler { + getMessageType: () => MessageType; + useProcessMessage: () => ( + chatData: ChatInputValues, + receiverId: string, + chatId: string + ) => Promise<{ content: string; messageType: MessageType }>; +} + +// @ts-expect-error Error +export const MessageHandlerProvider: Record = { + [MessageType.TEXT]: TextMessageHandler, + [MessageType.IMAGE]: ImageMessageHandler, +}; diff --git a/frontend/src/api/messages/strategies/TextMessageHandler.ts b/frontend/src/api/messages/strategies/TextMessageHandler.ts new file mode 100644 index 0000000..4fbb96f --- /dev/null +++ b/frontend/src/api/messages/strategies/TextMessageHandler.ts @@ -0,0 +1,15 @@ +import { MessageHandler } from "@api/messages/strategies/MessageHandler.ts"; +import { MessageType } from "@/types/Message.ts"; + +export const TextMessageHandler: MessageHandler = { + getMessageType: function () { + return MessageType.TEXT; + }, + useProcessMessage: function () { + return (chatData) => + Promise.resolve({ + content: chatData.message, + messageType: this.getMessageType(), + }); + }, +}; diff --git a/frontend/src/components/ChatListComponent.tsx b/frontend/src/components/ChatListComponent.tsx index f8805db..b670109 100644 --- a/frontend/src/components/ChatListComponent.tsx +++ b/frontend/src/components/ChatListComponent.tsx @@ -1,8 +1,9 @@ -import { MessagesService } from "@/api/messages"; +import { MessagesService } from "@api/messages/messages.ts"; import { MatchedUser } from "@/types/User"; import { Link } from "react-router-dom"; import UserAvatar from "./UserAvatar"; import { useMemo } from "react"; +import { MessageType } from "@/types/Message.ts"; interface ChatListComponentProps { matchedUser: MatchedUser; @@ -12,6 +13,20 @@ const formatMessage = (message: string) => { return message.slice(0, 30) + (message.length > 30 ? "..." : ""); }; +const getMessageContent = (content: string, messageType: MessageType) => { + switch (messageType) { + case MessageType.TEXT: + case MessageType.REPLY: + return content; + case MessageType.IMAGE: + return "Image"; + case MessageType.VOICE: + return "Voice"; + default: + return ""; + } +}; + const ChatListComponent = ({ matchedUser }: ChatListComponentProps) => { const { data } = MessagesService.useGetMessages(matchedUser.chatId); const { timestamp, message } = useMemo(() => { @@ -25,12 +40,16 @@ const ChatListComponent = ({ matchedUser }: ChatListComponentProps) => { const sentByCurrentUser = lastMessage.receiverId === matchedUser.id; const prefix = sentByCurrentUser ? "You: " : ""; const date = new Date(lastMessage.timestamp); + const lastMessageContent = getMessageContent( + lastMessage.content, + lastMessage.messageType + ); return { timestamp: date.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit", }), - message: `${prefix}${formatMessage(lastMessage.content)}`, + message: `${prefix}${formatMessage(lastMessageContent)}`, }; }, [data?.content]); return ( diff --git a/frontend/src/components/ChatMessageRecived.tsx b/frontend/src/components/ChatMessageRecived.tsx deleted file mode 100644 index b895029..0000000 --- a/frontend/src/components/ChatMessageRecived.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { ProjectedUser } from "@/types/User"; -import UserAvatar from "./UserAvatar"; - -interface ChatMessageProps { - text: string; - user: ProjectedUser; -} - -const ChatMessage = ({ text, user }: ChatMessageProps) => { - return ( -
-
- -
-
-

{user.firstName}

-

- {text} -

-
-
- ); -}; - -export default ChatMessage; diff --git a/frontend/src/components/ChatMessageSent.tsx b/frontend/src/components/ChatMessageSent.tsx deleted file mode 100644 index ab33ca8..0000000 --- a/frontend/src/components/ChatMessageSent.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { ProjectedUser } from "@/types/User"; -import { Message } from "@/types/Message"; - -interface ChatMessageProps { - message: Message; - user: ProjectedUser; -} - -const ChatMessage = ({ message }: ChatMessageProps): JSX.Element => { - // const timestamp = useMemo(() => { - // const date = new Date(message.timestamp); - // return `${date.getHours()}:${date.getMinutes()}`; - // }, [message.timestamp]); - return ( -
-

- {message.content} -

-
- ); -}; - -export default ChatMessage; diff --git a/frontend/src/components/ScrollToFieldError.tsx b/frontend/src/components/ScrollToFieldError.tsx deleted file mode 100644 index 6195d2a..0000000 --- a/frontend/src/components/ScrollToFieldError.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useFormikContext } from "formik"; -import { MutableRefObject, useEffect } from "react"; - -interface ScrollToFieldErrorProps { - pageRefs: MutableRefObject>; - inputPageLocation: Record; -} - -const ScrollToFieldError = ({ - pageRefs, - inputPageLocation, -}: ScrollToFieldErrorProps) => { - const { submitCount, isValid, errors } = useFormikContext(); - - useEffect(() => { - if (isValid || !submitCount) return; - - const mappedErrorsArray = ( - Object.keys(errors) as Array - ).map((key) => { - const page = inputPageLocation[key]; - return { error: key, page }; - }); - const errorWithLowestPage = mappedErrorsArray.reduce((prev, curr) => { - return curr.page < prev.page ? curr : prev; - }); - - const page = errorWithLowestPage.page; - const pageRef = pageRefs.current[page]; - if (!pageRef) return; - - pageRef.scrollIntoView({ behavior: "smooth" }); - - // Disable eslint because we only want to trigger on submitCount change - }, [submitCount]); // eslint-disable-line react-hooks/exhaustive-deps - - return null; -}; - -export default ScrollToFieldError; diff --git a/frontend/src/components/chat/ChatHistory.tsx b/frontend/src/components/chat/ChatHistory.tsx index 9011fbf..a8a31eb 100644 --- a/frontend/src/components/chat/ChatHistory.tsx +++ b/frontend/src/components/chat/ChatHistory.tsx @@ -1,4 +1,4 @@ -import { MessagesService } from "@/api/messages"; +import { MessagesService } from "@api/messages/messages.ts"; import React, { useEffect } from "react"; import ChatMessage from "./ChatMessage"; import { MatchedUser } from "@/types/User"; diff --git a/frontend/src/components/chat/ChatInput.tsx b/frontend/src/components/chat/ChatInput.tsx index 9ceaab6..e2efae3 100644 --- a/frontend/src/components/chat/ChatInput.tsx +++ b/frontend/src/components/chat/ChatInput.tsx @@ -1,28 +1,24 @@ -import {Field, Form, Formik, FormikHelpers, useField} from "formik"; +import { Field, Form, Formik, FormikHelpers, useField } from "formik"; import * as Yup from "yup"; import SendIcon from "@mui/icons-material/Send"; import PhotoCameraIcon from "@mui/icons-material/PhotoCamera"; -import CloseIcon from '@mui/icons-material/Close'; -import {useMemo} from "react"; - -export interface ChatInputValues { - message: string; - image: File | null; -} +import CloseIcon from "@mui/icons-material/Close"; +import { useMemo } from "react"; +import { ChatInputValues } from "@/types/Message.ts"; const chatValidation = Yup.object({ message: Yup.string(), }); interface ChatInputProps { - handleSubmit: (values: ChatInputValues, helpers: FormikHelpers) => void; + handleSubmit: ( + values: ChatInputValues, + helpers: FormikHelpers + ) => void; initialValues: ChatInputValues; } -const ChatInput = ({ - handleSubmit, - initialValues, - }: ChatInputProps) => { +const ChatInput = ({ handleSubmit, initialValues }: ChatInputProps) => { const submitOnEnter = ( e: React.KeyboardEvent, submitForm: () => Promise @@ -38,13 +34,13 @@ const ChatInput = ({ validationSchema={chatValidation} onSubmit={handleSubmit} > - {({submitForm, setFieldValue}) => ( + {({ submitForm, setFieldValue }) => (
- setFieldValue("image", null)}/> + setFieldValue("image", null)} />
-
@@ -74,7 +79,7 @@ const ChatInput = ({ ); }; -const ChatImageDisplay = ({onRemove}: { onRemove: () => void }) => { +const ChatImageDisplay = ({ onRemove }: { onRemove: () => void }) => { const [field] = useField("image"); const imageUrl = useMemo(() => { if (!field.value) return null; @@ -90,13 +95,16 @@ const ChatImageDisplay = ({onRemove}: { onRemove: () => void }) => { alt="Chat image" className="max-w-24 h-24 object-cover rounded-lg" /> -
); -} +}; export default ChatInput; diff --git a/frontend/src/components/chat/ChatMessage.tsx b/frontend/src/components/chat/ChatMessage.tsx index 51d67a1..da27f32 100644 --- a/frontend/src/components/chat/ChatMessage.tsx +++ b/frontend/src/components/chat/ChatMessage.tsx @@ -1,4 +1,4 @@ -import { Message } from "@/types/Message"; +import { Message, MessageType } from "@/types/Message"; import { useMemo } from "react"; import UserAvatar from "../UserAvatar"; import { ProjectedUser } from "@/types/User"; @@ -26,7 +26,7 @@ const ChatMessage = ({ message, matchedUser }: ChatMessageProps) => { ); }; -const ChatMessageSent = ({ message }: ChatMessageProps): JSX.Element => { +const ChatMessageSent = ({ message }: ChatMessageProps) => { // const timestamp = useMemo(() => { // const date = new Date(message.timestamp); // return `${date.getHours()}:${date.getMinutes()}`; @@ -34,7 +34,7 @@ const ChatMessageSent = ({ message }: ChatMessageProps): JSX.Element => { return (

- {message.content} +

); @@ -49,11 +49,26 @@ const ChatMessageReceived = ({ matchedUser, message }: ChatMessageProps) => {

{matchedUser.firstName}

- {message.content} +

); }; +const ChatMessageContent = ({ + message, +}: Omit) => { + switch (message.messageType) { + case MessageType.TEXT: + return <>{message.content}; + case MessageType.IMAGE: + return ( + chat image + ); + default: + return null; + } +}; + export default ChatMessage; diff --git a/frontend/src/types/Message.ts b/frontend/src/types/Message.ts index 3e7d014..7e8d82d 100644 --- a/frontend/src/types/Message.ts +++ b/frontend/src/types/Message.ts @@ -5,16 +5,39 @@ export interface Message { receiverId: string; content: string; timestamp: string; + messageType: MessageType; } export enum MessageType { - TEXT, - IMAGE, - VOICE, - REPLY + TEXT = "TEXT", + IMAGE = "IMAGE", + VOICE = "VOICE", + REPLY = "REPLY", } +/** + * Messages are processed in the order of the array. + * This is important when sending text and an image in the same message. + * The image should be sent first, then the text. + */ +export const SUPPORTED_MESSAGE_TYPES = [ + MessageType.IMAGE, + MessageType.TEXT, +] as const; + export interface MessageImageUploadData { chatId: string; photo: File; } + +export interface MessageData { + content: string; + receiverId: string; + chatId: string; + messageType: MessageType; +} + +export interface ChatInputValues { + message: string; + image: File | null; +} diff --git a/frontend/src/views/ChatPage.tsx b/frontend/src/views/ChatPage.tsx index 11fd120..cd50f79 100644 --- a/frontend/src/views/ChatPage.tsx +++ b/frontend/src/views/ChatPage.tsx @@ -1,47 +1,47 @@ -import {MessagesService} from "@/api/messages"; +import { MessagesService } from "@api/messages/messages.ts"; import UserAvatar from "@/components/UserAvatar"; -import {ProjectedUser} from "@/types/User"; -import {FormikHelpers} from "formik"; -import {useEffect, useRef} from "react"; -import {Link, Navigate, useParams} from "react-router-dom"; +import { ProjectedUser } from "@/types/User"; +import { FormikHelpers } from "formik"; +import { useEffect, useRef } from "react"; +import { Link, Navigate, useParams } from "react-router-dom"; import KeyboardArrowLeftIcon from "@mui/icons-material/KeyboardArrowLeft"; import React from "react"; import ChatMessage from "@/components/chat/ChatMessage"; import ChatHistory from "@/components/chat/ChatHistory"; -import {CircularProgress} from "@mui/material"; -import ChatInput, {ChatInputValues} from "@/components/chat/ChatInput"; -import {useInView} from "react-intersection-observer"; +import { CircularProgress } from "@mui/material"; +import ChatInput from "@/components/chat/ChatInput"; +import { useInView } from "react-intersection-observer"; import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; -import {useAutoAnimate} from "@formkit/auto-animate/react"; +import { useAutoAnimate } from "@formkit/auto-animate/react"; import UserActionsDropdown from "@/components/UserActionsDropdown"; -import {MatchesService} from "@/api/matches"; +import { MatchesService } from "@/api/matches"; +import { ChatInputValues } from "@/types/Message.ts"; const initialValues = { message: "", - image: null + image: null, }; - const ChatPage = () => { - const [messagesStartRef, messagesStartInView] = useInView({delay: 500}); - const [messagesEndRef, messagesEndInView] = useInView({triggerOnce: true}); + const [messagesStartRef, messagesStartInView] = useInView({ delay: 500 }); + const [messagesEndRef, messagesEndInView] = useInView({ triggerOnce: true }); const startRef = useRef(null); const [parent] = useAutoAnimate(); - const {userId} = useParams() as { userId: string }; + const { userId } = useParams() as { userId: string }; const userQuery = MatchesService.useGetMatchedUserById(userId); const recentMessages = MessagesService.useGetMessages(userQuery.data?.chatId); - const sendMessage = MessagesService.useSendMessage(); + const sendChat = MessagesService.useSendChat(); useEffect(() => { if (!recentMessages.isSuccess) { - startRef?.current?.scrollIntoView({behavior: "smooth"}); + startRef?.current?.scrollIntoView({ behavior: "smooth" }); } }, [recentMessages.isSuccess]); useEffect(() => { if (messagesStartInView && recentMessages.isSuccess) { - startRef?.current?.scrollIntoView({behavior: "smooth"}); + startRef?.current?.scrollIntoView({ behavior: "smooth" }); } }, [ messagesStartInView, @@ -53,25 +53,23 @@ const ChatPage = () => { return (
- +
); } if (userQuery.isError || !userQuery.isSuccess) { - return ; + return ; } const user = userQuery.data; - const handleSubmit = ( + const handleSubmit = async ( values: ChatInputValues, helpers: FormikHelpers ) => { - console.log(values) - return; - sendMessage(values.message, user.id, user.chatId); + await sendChat(values, user.id, user.chatId); helpers.resetForm(); }; @@ -79,7 +77,7 @@ const ChatPage = () => { return (
- +
); @@ -95,11 +93,11 @@ const ChatPage = () => { )} @@ -116,7 +114,7 @@ const ChatPage = () => {
{recentMessages.data.content.map((message) => ( - + ))}
{!recentMessages.data.last && messagesEndInView && ( @@ -126,7 +124,6 @@ const ChatPage = () => { matchedUser={user} /> )} - ); @@ -137,15 +134,13 @@ interface ChatPageHeaderProps { user?: ProjectedUser; } -const ChatPageHeader = ({children, user}: ChatPageHeaderProps) => ( -
+const ChatPageHeader = ({ children, user }: ChatPageHeaderProps) => ( +
{user && ( -
+
- + ( to={`/matches/profile/${user.id}`} className="flex items-center justify-center w-12 h-12 bg-gray-300 rounded-full" > - + - +
)} diff --git a/frontend/src/views/ListOfMatchesForChatPage.tsx b/frontend/src/views/ListOfMatchesForChatPage.tsx index 2546849..78f5ca5 100644 --- a/frontend/src/views/ListOfMatchesForChatPage.tsx +++ b/frontend/src/views/ListOfMatchesForChatPage.tsx @@ -4,7 +4,7 @@ import * as MessagesCard from "@/components/GenericCard"; import { useAutoAnimate } from "@formkit/auto-animate/react"; import useCurrentUserContext from "@/hooks/useCurrentUser"; -import { MessagesService } from "@/api/messages"; +import { MessagesService } from "@api/messages/messages.ts"; import { MatchesService } from "@/api/matches"; const ListOfMatchesForChatPage = () => { diff --git a/frontend/src/views/chat/ChatLayout.tsx b/frontend/src/views/chat/ChatLayout.tsx index 32180f3..8af24b7 100644 --- a/frontend/src/views/chat/ChatLayout.tsx +++ b/frontend/src/views/chat/ChatLayout.tsx @@ -1,4 +1,4 @@ -import { MessagesService } from "@/api/messages"; +import { MessagesService } from "@api/messages/messages.ts"; import useCurrentUserContext from "@/hooks/useCurrentUser"; import { useQueryClient } from "@tanstack/react-query"; import { useEffect } from "react"; @@ -13,7 +13,7 @@ const ChatLayout = () => { return () => { queryClient.invalidateQueries({ queryKey: ["messages"] }); }; - }, []); + }, [queryClient]); return (
From 62e21c64f130876cc87e9021eb78f51856c5de73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Fri, 7 Jun 2024 13:36:47 +0200 Subject: [PATCH 7/7] Fixing mobile errors and adding chat image upload endpoint. --- frontend/src/components/ProfileCard.tsx | 1 + mobileV2/src/api/messages.ts | 101 +++++++++++++----- mobileV2/src/api/users.ts | 2 +- .../(steps)/Step4Preference.tsx | 3 +- .../app/(Forms)/SetupPreferences/store.tsx | 2 +- .../(tabs)/Settings/(screens)/Gallery.tsx | 3 +- .../Settings/(screens)/ProfilePicture.tsx | 7 +- .../(tabs)/Settings/(screens)/Theme.tsx | 8 +- .../Settings/(screens)/UserSettings.tsx | 9 +- mobileV2/src/components/GenderPicker.tsx | 6 +- mobileV2/src/components/MatchCard.tsx | 44 ++++---- mobileV2/src/components/ProfileCard.tsx | 28 +---- mobileV2/src/context/ThemeProvider.tsx | 41 +++---- mobileV2/src/hooks/useTheme.tsx | 2 +- mobileV2/src/types/Message.d.ts | 8 -- mobileV2/src/types/Message.ts | 22 ++++ mobileV2/src/types/User.d.ts | 16 +-- 17 files changed, 169 insertions(+), 134 deletions(-) delete mode 100644 mobileV2/src/types/Message.d.ts create mode 100644 mobileV2/src/types/Message.ts diff --git a/frontend/src/components/ProfileCard.tsx b/frontend/src/components/ProfileCard.tsx index f388dc2..72f8774 100644 --- a/frontend/src/components/ProfileCard.tsx +++ b/frontend/src/components/ProfileCard.tsx @@ -64,6 +64,7 @@ const ProfileCard = ({ srcSet={user.profileImageUrl || "/Default_pfp.svg"} className="w-full object-cover md:object-contain max-h-[33rem] md:max-h-[26rem] sm:rounded-t-lg" loading="lazy" + alt="user profile picture" />
diff --git a/mobileV2/src/api/messages.ts b/mobileV2/src/api/messages.ts index b915aad..9995bc4 100644 --- a/mobileV2/src/api/messages.ts +++ b/mobileV2/src/api/messages.ts @@ -1,11 +1,18 @@ import useAxiosPrivate from "@/hooks/useAxiosPrivate"; -import {useInfiniteQuery, useQuery, useQueryClient,} from "@tanstack/react-query"; -import {useStompClient, useSubscription} from "react-stomp-hooks"; +import { + useInfiniteQuery, + useMutation, + UseMutationOptions, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { useStompClient, useSubscription } from "react-stomp-hooks"; import useAuth from "@/hooks/useAuth"; -import {MatchedUser} from "@/types/User"; -import {Message} from "@/types/Message"; -import {Page} from "@/types/Page"; +import { MatchedUser } from "@/types/User"; +import { Message, MessageImageUploadData, MessageType } from "@/types/Message"; +import { Page } from "@/types/Page"; import useCurrentUserContext from "@/hooks/useCurrentUser"; +import ReactNativeBlobUtil from "react-native-blob-util"; export const HISTORY_PAGE_SIZE = 15; export const MESSAGE_PAGE_SIZE = 15; @@ -17,36 +24,37 @@ export const MessagesService = { const { auth } = useAuth(); const currentUser = useCurrentUserContext(); - return ( - content: string, - receiverId: string, - chatId: string - ) => { + return (content: string, receiverId: string, chatId: string) => { if (!client) { throw new Error("Stomp client not initialized"); } client.publish({ destination: `/app/send-message`, - body: JSON.stringify({content, receiverId, chatId}), + body: JSON.stringify({ + content, + receiverId, + chatId, + messageType: MessageType.TEXT, + }), headers: { Authorization: `Bearer ${auth?.accessToken}`, }, }); queryClient.setQueryData( - ["messages", chatId], - (oldData: Page) => { - const newContent = [...oldData.content]; - newContent.unshift({ - id: new Date().getTime().toString() + content, - content, - senderId: currentUser.id, - receiverId, - chatId, - timestamp: new Date().toISOString(), - }); - return {...oldData, content: newContent}; - } + ["messages", chatId], + (oldData: Page) => { + const newContent = [...oldData.content]; + newContent.unshift({ + id: new Date().getTime().toString() + content, + content, + senderId: currentUser.id, + receiverId, + chatId, + timestamp: new Date().toISOString(), + }); + return { ...oldData, content: newContent }; + } ); }; }, @@ -120,4 +128,49 @@ export const MessagesService = { ); }); }, + + useUploadImage: < + Response extends string, + Err extends Error, + Args extends MessageImageUploadData, + >( + options?: Omit, "mutationFn"> + ) => { + const axios = useAxiosPrivate(); + const { auth } = useAuth(); + + return useMutation({ + mutationFn: async (data) => { + const response = await ReactNativeBlobUtil.fetch( + "POST", + `${axios.defaults.baseURL}/messages/upload-image`, + { + Authorization: `Bearer ${auth?.accessToken}`, + "Content-Type": "multipart/form-data", + }, + [ + { + name: "chatId", + data: data.chatId, + }, + { + name: "photo", + filename: data.photo.fileName, + type: data.photo.type, + data: data.photo.base64, + }, + ] + ); + + if (response.respInfo.status >= 400) { + throw new Error(response.data); + } + return response.json(); + }, + onError: (error) => { + console.error("Mutation error:", error); + }, + ...options, + }); + }, }; diff --git a/mobileV2/src/api/users.ts b/mobileV2/src/api/users.ts index 0ff4490..43441fa 100644 --- a/mobileV2/src/api/users.ts +++ b/mobileV2/src/api/users.ts @@ -7,13 +7,13 @@ import { import useAuth from "@/hooks/useAuth"; import useAxiosPrivate from "@/hooks/useAxiosPrivate"; import { - Asset, MatchedUser, PreferencesInitData, User, UserUpdateData, } from "@/types/User"; import ReactNativeBlobUtil from "react-native-blob-util"; +import { Asset } from "react-native-image-picker"; export const UsersService = { useGetCurrentUser() { diff --git a/mobileV2/src/app/(Forms)/SetupPreferences/(steps)/Step4Preference.tsx b/mobileV2/src/app/(Forms)/SetupPreferences/(steps)/Step4Preference.tsx index 09a3970..a7ee792 100644 --- a/mobileV2/src/app/(Forms)/SetupPreferences/(steps)/Step4Preference.tsx +++ b/mobileV2/src/app/(Forms)/SetupPreferences/(steps)/Step4Preference.tsx @@ -1,12 +1,11 @@ import { NavigationProp, useIsFocused } from "@react-navigation/native"; import React, { useEffect } from "react"; import { View, Text, Image, ScrollView } from "react-native"; -import { launchImageLibrary } from "react-native-image-picker"; +import { Asset, launchImageLibrary } from "react-native-image-picker"; import { WizardStore } from "../store"; import { useForm } from "react-hook-form"; import { Button, ProgressBar } from "react-native-paper"; import { StyleSheet } from "react-native"; -import { Asset } from "@/types/User"; import { useTheme } from "@/context/ThemeProvider"; type Step1PreferencesProps = { diff --git a/mobileV2/src/app/(Forms)/SetupPreferences/store.tsx b/mobileV2/src/app/(Forms)/SetupPreferences/store.tsx index 83226f1..cf24ab2 100644 --- a/mobileV2/src/app/(Forms)/SetupPreferences/store.tsx +++ b/mobileV2/src/app/(Forms)/SetupPreferences/store.tsx @@ -1,5 +1,5 @@ -import { Asset } from "@/types/User"; import { registerInDevtools, Store } from "pullstate"; +import { Asset } from "react-native-image-picker"; export interface storeTypes { description: string; diff --git a/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/Gallery.tsx b/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/Gallery.tsx index 30c3741..8a2167c 100644 --- a/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/Gallery.tsx +++ b/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/Gallery.tsx @@ -9,7 +9,7 @@ import { Modal, } from "react-native"; import { Entypo } from "@expo/vector-icons"; -import { launchImageLibrary } from "react-native-image-picker"; +import { Asset, launchImageLibrary } from "react-native-image-picker"; import { Formik, FormikConfig } from "formik"; import useCurrentUserContext from "../../../../../hooks/useCurrentUser"; import { FlatList } from "react-native-gesture-handler"; @@ -17,7 +17,6 @@ import { EvilIcons } from "@expo/vector-icons"; import { Button } from "react-native-paper"; import { useTheme } from "@/context/ThemeProvider"; import { UsersService } from "@api/users"; -import { Asset } from "@/types/User"; const screenWidth = Dimensions.get("window").width; const styles = StyleSheet.create({ diff --git a/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/ProfilePicture.tsx b/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/ProfilePicture.tsx index 343d9c5..ce5e399 100644 --- a/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/ProfilePicture.tsx +++ b/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/ProfilePicture.tsx @@ -1,14 +1,13 @@ -import { UsersService } from "./../../../../../api/users"; -import useCurrentUserContext from "../../../../../hooks/useCurrentUser"; +import { UsersService } from "@api/users"; import { Formik, FormikHelpers } from "formik"; import { View, Image } from "react-native"; import { Button } from "react-native-paper"; -import { launchImageLibrary } from "react-native-image-picker"; +import { Asset, launchImageLibrary } from "react-native-image-picker"; import React from "react"; import { MaterialIcons } from "@expo/vector-icons"; import { TouchableOpacity } from "react-native-gesture-handler"; import { useTheme } from "@/context/ThemeProvider"; -import { Asset } from "@/types/User"; +import useCurrentUserContext from "@hooks/useCurrentUser"; const SettingsProfilePicture = () => { const user = useCurrentUserContext(); diff --git a/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/Theme.tsx b/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/Theme.tsx index a2a05b5..c88f462 100644 --- a/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/Theme.tsx +++ b/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/Theme.tsx @@ -1,4 +1,4 @@ -import { Theme } from "@/context/ThemeProvider"; +import { ThemeType } from "@/context/ThemeProvider"; import useTheme from "../../../../../hooks/useTheme"; import { RadioButton } from "react-native-paper"; import { View } from "react-native"; @@ -6,11 +6,11 @@ import { useState } from "react"; import { Text } from "react-native"; const SettingsTheme = () => { - const { theme, setThemeMode } = useTheme(); + const { setThemeType } = useTheme(); const [checked, setChecked] = useState("light"); - const handleChange = (value: Theme) => { - setThemeMode(value); + const handleChange = (value: ThemeType) => { + setThemeType(value); console.log("Radio button value", value); }; diff --git a/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/UserSettings.tsx b/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/UserSettings.tsx index c104a9e..c8c35f3 100644 --- a/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/UserSettings.tsx +++ b/mobileV2/src/app/(Protected)/(tabs)/Settings/(screens)/UserSettings.tsx @@ -1,14 +1,12 @@ import { useState } from "react"; -import useCurrentUserContext from "../../../../../hooks/useCurrentUser"; import { Formik, FormikHelpers } from "formik"; import * as Yup from "yup"; import { UsersService } from "@api/users"; - -/* import TagInput from "@/components/forms/TagInput"; */ -import SaveCancelButtons from "../../../../../components/SaveCancelButtons"; import { View, Text, ScrollView } from "react-native"; import { HelperText, TextInput } from "react-native-paper"; import { useTheme } from "@/context/ThemeProvider"; +import useCurrentUserContext from "@hooks/useCurrentUser"; +import SaveCancelButtons from "@components/SaveCancelButtons"; const updatePreferenceSchema = Yup.object({ phoneNumber: Yup.number().required("Required"), @@ -39,7 +37,7 @@ interface UserProfileUpdateData { type ResetFormFunction = FormikHelpers["resetForm"]; -const UserSettings = ({ navigation }: { navigation: any }) => { +const UserSettings = () => { const user = useCurrentUserContext(); const [editMode, setEditMode] = useState(false); const { mutateAsync: updateUser } = UsersService.useUpdateUser(); @@ -79,7 +77,6 @@ const UserSettings = ({ navigation }: { navigation: any }) => { {({ handleChange, handleBlur, - handleSubmit, isSubmitting, values, errors, diff --git a/mobileV2/src/components/GenderPicker.tsx b/mobileV2/src/components/GenderPicker.tsx index ebf2337..8dbd9ae 100644 --- a/mobileV2/src/components/GenderPicker.tsx +++ b/mobileV2/src/components/GenderPicker.tsx @@ -2,9 +2,10 @@ import { useMemo, useState } from "react"; import { useField } from "formik"; import { View } from "react-native"; -import { HelperText, Text, useTheme } from "react-native-paper"; +import { HelperText, useTheme } from "react-native-paper"; import DropDownPicker from "react-native-dropdown-picker"; + const GenderPicker = () => { const genderOptions = [ { label: "Male", value: "M" }, @@ -33,7 +34,6 @@ const GenderPicker = () => { return ( - { ); }; -export default GenderPicker; \ No newline at end of file +export default GenderPicker; diff --git a/mobileV2/src/components/MatchCard.tsx b/mobileV2/src/components/MatchCard.tsx index 8266c95..05a7a2a 100644 --- a/mobileV2/src/components/MatchCard.tsx +++ b/mobileV2/src/components/MatchCard.tsx @@ -4,15 +4,13 @@ import { Text, Image, StyleSheet, - TouchableOpacity, TouchableHighlight, } from "react-native"; -import { ProjectedUser } from "../types/User"; +import type { ProjectedUser } from "@/types/User"; import { AntDesign } from "@expo/vector-icons"; import { Entypo } from "@expo/vector-icons"; - interface MatchCardProps { user: ProjectedUser; loading: boolean; @@ -21,36 +19,42 @@ interface MatchCardProps { } const MatchCard = ({ user, - loading, handleNextUser, openDetailedProfile, }: MatchCardProps) => { return ( - - - - {user.firstName}, {user.age} - {user.location} + + + + + {user.firstName}, {user.age} + + {user.location} + - - handleNextUser(false, user.id)}> + handleNextUser(false, user.id)} + > - - handleNextUser(true, user.id)}> + + handleNextUser(true, user.id)} + > - {}}> + {}}> diff --git a/mobileV2/src/components/ProfileCard.tsx b/mobileV2/src/components/ProfileCard.tsx index 8c6b995..e7fb311 100644 --- a/mobileV2/src/components/ProfileCard.tsx +++ b/mobileV2/src/components/ProfileCard.tsx @@ -7,8 +7,8 @@ import { SafeAreaView, ScrollView, } from "react-native"; -import { TouchableOpacity } from "react-native-gesture-handler"; // Assuming you have TouchableOpacity from react-native-gesture-handler -import { ProjectedUser } from "../types/User"; +import { TouchableOpacity } from "react-native-gesture-handler"; +import { ProjectedUser } from "@/types/User"; import { AntDesign } from "@expo/vector-icons"; import { Entypo } from "@expo/vector-icons"; import { StyleSheet } from "react-native"; @@ -21,10 +21,6 @@ interface ProfileCardProps { showChatIcon?: boolean; handleNextUser: (matchAccept: boolean, userId: string) => void; } -interface MatchedTag { - value: string; - matched: boolean; -} const ProfileCard = ({ user, @@ -296,25 +292,5 @@ const styles = StyleSheet.create({ elevation: 5, }, }); -const Tag = ({ tag }: { tag: any }) => { - const tagStyle = { - paddingVertical: 5, - paddingHorizontal: 10, - borderRadius: 10, - minWidth: 80, - borderWidth: 1, - borderColor: tag.matched ? "#FF0000" : "#ccc", - backgroundColor: tag.matched ? "#FF0000" : "#fff", - flexDirection: "row", - alignItems: "center", - }; - - return ( - - {tag.matched} - {tag.value} - - ); -}; export default ProfileCard; diff --git a/mobileV2/src/context/ThemeProvider.tsx b/mobileV2/src/context/ThemeProvider.tsx index a778c0b..0fb6e64 100644 --- a/mobileV2/src/context/ThemeProvider.tsx +++ b/mobileV2/src/context/ThemeProvider.tsx @@ -1,14 +1,20 @@ -import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; -import {useColorScheme} from 'react-native'; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import { useColorScheme } from "react-native"; import { Provider as PaperProvider, MD3DarkTheme as PaperDarkTheme, MD3LightTheme as DefaultTheme, -} from 'react-native-paper'; +} from "react-native-paper"; import { DarkTheme as NavigationDarkTheme, DefaultTheme as NavigationDefaultTheme, -} from '@react-navigation/native'; +} from "@react-navigation/native"; const lightTheme = { ...NavigationDefaultTheme, @@ -18,7 +24,7 @@ const lightTheme = { primary: "#f2f3f5", secondary: "#000000", accent: "#EE5253", - tertiary: "#ebebeb" + tertiary: "#ebebeb", }, }; @@ -30,12 +36,12 @@ const darkTheme = { primary: "#1E1E1E", secondary: "#FFFFFF", accent: "#EE5253", - tertiary: "#2b2b2b" + tertiary: "#2b2b2b", }, }; -export type Theme = typeof lightTheme; -export type ThemeType = 'dark' | 'light'; +export type Theme = typeof lightTheme; +export type ThemeType = "dark" | "light" | "system"; export interface ThemeContextValue { theme: Theme; @@ -47,7 +53,7 @@ export interface ThemeContextValue { export const ThemeContext = React.createContext({ theme: lightTheme, - themeType: 'light', + themeType: "light", isDarkTheme: false, setThemeType: () => {}, toggleThemeType: () => {}, @@ -59,21 +65,21 @@ export interface ThemeContextProviderProps { children: React.ReactNode; } -export const ThemeProvider = ({children}: ThemeContextProviderProps) => { +export const ThemeProvider = ({ children }: ThemeContextProviderProps) => { const systemTheme = useColorScheme(); - const [themeType, setThemeType] = useState(systemTheme || 'light'); + const [themeType, setThemeType] = useState(systemTheme || "light"); useEffect(() => { if (!themeType || themeType === systemTheme) { - setThemeType(systemTheme || 'light'); + setThemeType(systemTheme || "light"); } }, [systemTheme, themeType]); const toggleThemeType = useCallback(() => { - setThemeType(prev => (prev === 'dark' ? 'light' : 'dark')); + setThemeType((prev) => (prev === "dark" ? "light" : "dark")); }, []); - const isDarkTheme = useMemo(() => themeType === 'dark', [themeType]); + const isDarkTheme = useMemo(() => themeType === "dark", [themeType]); const theme = useMemo( () => (isDarkTheme ? darkTheme : lightTheme), [isDarkTheme] @@ -87,10 +93,9 @@ export const ThemeProvider = ({children}: ThemeContextProviderProps) => { isDarkTheme, setThemeType, toggleThemeType, - }}> - - {children} - + }} + > + {children} ); }; diff --git a/mobileV2/src/hooks/useTheme.tsx b/mobileV2/src/hooks/useTheme.tsx index 89272ba..f31bdbb 100644 --- a/mobileV2/src/hooks/useTheme.tsx +++ b/mobileV2/src/hooks/useTheme.tsx @@ -1,4 +1,4 @@ -import { ThemeContext } from "../context/ThemeProvider"; +import { ThemeContext } from "@/context/ThemeProvider"; import { useContext } from "react"; const useTheme = () => useContext(ThemeContext); diff --git a/mobileV2/src/types/Message.d.ts b/mobileV2/src/types/Message.d.ts deleted file mode 100644 index 491b6d9..0000000 --- a/mobileV2/src/types/Message.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface Message { - id: string; - chatId: string; - senderId: string; - receiverId: string; - content: string; - timestamp: string; -} diff --git a/mobileV2/src/types/Message.ts b/mobileV2/src/types/Message.ts new file mode 100644 index 0000000..6714ba8 --- /dev/null +++ b/mobileV2/src/types/Message.ts @@ -0,0 +1,22 @@ +import { Asset } from "react-native-image-picker"; + +export interface Message { + id: string; + chatId: string; + senderId: string; + receiverId: string; + content: string; + timestamp: string; +} + +export enum MessageType { + TEXT = "TEXT", + IMAGE = "IMAGE", + VOICE = "VOICE", + REPLY = "REPLY", +} + +export interface MessageImageUploadData { + chatId: string; + photo: Asset; +} diff --git a/mobileV2/src/types/User.d.ts b/mobileV2/src/types/User.d.ts index 35d6be9..54483f2 100644 --- a/mobileV2/src/types/User.d.ts +++ b/mobileV2/src/types/User.d.ts @@ -1,3 +1,5 @@ +import { Asset } from "react-native-image-picker"; + export interface User { id: string; email: string; @@ -69,17 +71,3 @@ export interface PreferencesInitData { }; photo: Asset; } -export interface Asset { - base64?: string; - uri?: string; - width?: number; - height?: number; - originalPath?: string; - fileSize?: number; - type?: string; - fileName?: string; - duration?: number; - bitrate?: number; - timestamp?: string; - id?: string; -}