From 42fb086d02bc54ea42f47a6fc725fede15920d67 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 14 Apr 2026 20:25:54 -0600 Subject: [PATCH] Fix chat attachments and avatars --- .../controller/UserAvatarController.java | 2 +- .../backend/service/AvatarStorageService.java | 3 +- .../example/petshopdesktop/api/ApiClient.java | 33 +++++++++++++ .../petshopdesktop/api/endpoints/ChatApi.java | 6 +++ .../controllers/ChatController.java | 46 +++++++++++-------- 5 files changed, 68 insertions(+), 22 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java b/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java index bb5c9342..b236cf8b 100644 --- a/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java +++ b/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java @@ -24,7 +24,7 @@ public class UserAvatarController { } @GetMapping("/{userId}/avatar/file") - @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") + @PreAuthorize("isAuthenticated()") public ResponseEntity getUserAvatarFile(@PathVariable Long userId) { User user = userRepository.findById(userId).orElse(null); if (user == null || !avatarStorageService.hasAvatar(user)) { diff --git a/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java b/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java index 2229c3d4..5c3b4e08 100644 --- a/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java +++ b/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java @@ -22,7 +22,6 @@ import java.util.UUID; public class AvatarStorageService { private static final String STORED_PREFIX = "/uploads/avatars/"; - private static final String OWNER_ENDPOINT = "/api/v1/auth/me/avatar/file"; @Value("${app.upload.base-dir:uploads}") private String uploadBaseDir; @@ -65,7 +64,7 @@ public class AvatarStorageService { } public String toOwnerAvatarUrl(User user) { - return hasAvatar(user) ? OWNER_ENDPOINT : null; + return hasAvatar(user) ? "/api/v1/users/" + user.getId() + "/avatar/file" : null; } public String toStoredAvatarUrl(String avatarFilenamePath) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java b/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java index 3914d226..50a14bce 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.example.petshopdesktop.auth.UserSession; +import java.io.ByteArrayOutputStream; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -142,6 +143,38 @@ public class ApiClient { return handleResponse(response, responseClass); } + public T postMultipartWithText(String path, String filePartName, Path filePath, + String textPartName, String textContent, + Class responseClass) throws Exception { + String boundary = "----PetShopDesktop" + UUID.randomUUID(); + String mimeType = Files.probeContentType(filePath); + if (mimeType == null || mimeType.isBlank()) mimeType = "application/octet-stream"; + + byte[] fileBytes = Files.readAllBytes(filePath); + String fileName = filePath.getFileName().toString(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + filePartName + + "\"; filename=\"" + fileName + "\"\r\nContent-Type: " + mimeType + "\r\n\r\n") + .getBytes(StandardCharsets.UTF_8)); + out.write(fileBytes); + out.write("\r\n".getBytes(StandardCharsets.UTF_8)); + if (textContent != null && !textContent.isBlank()) { + out.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + textPartName + + "\"\r\n\r\n" + textContent + "\r\n").getBytes(StandardCharsets.UTF_8)); + } + out.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8)); + + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + path)) + .header("Content-Type", "multipart/form-data; boundary=" + boundary) + .POST(HttpRequest.BodyPublishers.ofByteArray(out.toByteArray())) + .timeout(Duration.ofSeconds(30)); + addAuthHeader(builder); + HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + return handleResponse(response, responseClass); + } + public T put(String path, Object requestBody, Class responseClass) throws Exception { String jsonBody = objectMapper.writeValueAsString(requestBody); diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ChatApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ChatApi.java index 37d3e2a5..3d58547e 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ChatApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ChatApi.java @@ -7,6 +7,7 @@ import org.example.petshopdesktop.api.dto.chat.ConversationResponse; import org.example.petshopdesktop.api.dto.chat.MessageRequest; import org.example.petshopdesktop.api.dto.chat.MessageResponse; import org.example.petshopdesktop.api.dto.chat.UpdateConversationRequest; +import java.io.File; import java.util.List; public class ChatApi { @@ -43,6 +44,11 @@ public class ChatApi { return apiClient.post("/api/v1/chat/conversations/" + conversationId + "/messages", request, MessageResponse.class); } + public MessageResponse sendMessageWithAttachment(Long conversationId, File file, String content) throws Exception { + String path = "/api/v1/chat/conversations/" + conversationId + "/attachments"; + return apiClient.postMultipartWithText(path, "file", file.toPath(), "content", content, MessageResponse.class); + } + public ConversationResponse updateConversation(Long id, UpdateConversationRequest request) throws Exception { return apiClient.put("/api/v1/chat/conversations/" + id, request, ConversationResponse.class); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java index 6d1082fd..17033f16 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java @@ -27,6 +27,7 @@ import org.example.petshopdesktop.api.dto.chat.UpdateConversationRequest; import org.example.petshopdesktop.api.dto.common.DropdownOption; import org.example.petshopdesktop.api.endpoints.ChatApi; import org.example.petshopdesktop.api.endpoints.DropdownApi; +import org.example.petshopdesktop.api.ApiClient; import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.util.ActivityLogger; @@ -176,34 +177,33 @@ public class ChatController { } String content = txtMessage.getText() == null ? "" : txtMessage.getText().trim(); - if (content.isEmpty()) { - if (selectedAttachmentFile != null) { - lblChatStatus.setText("Attachments are not available yet"); - } + if (content.isEmpty() && selectedAttachmentFile == null) { return; } Long convId = selectedConversation.getId(); + File attachmentFile = selectedAttachmentFile; txtMessage.clear(); btnSend.setDisable(true); - lblChatStatus.setText("Sending message..."); new Thread(() -> { try { - MessageRequest request = new MessageRequest(content); - MessageResponse response = ChatApi.getInstance().sendMessage(convId, request); + MessageResponse response; + if (attachmentFile != null) { + response = ChatApi.getInstance().sendMessageWithAttachment(convId, attachmentFile, content); + } else { + response = ChatApi.getInstance().sendMessage(convId, new MessageRequest(content)); + } Platform.runLater(() -> { btnSend.setDisable(false); realtimeClient.markConversationReplied(convId, UserSession.getInstance().getUserId()); appendMessageIfSelected(response); - if (selectedAttachmentFile != null) { + if (attachmentFile != null) { clearLocalAttachment(); - lblChatStatus.setText("Message sent without attachment"); - } else { - lblChatStatus.setText("Message sent"); } + lblChatStatus.setText("Message sent"); }); } catch (Exception e) { Platform.runLater(() -> { @@ -507,14 +507,16 @@ public class ChatController { Circle avatar = new Circle(16); avatar.setFill(javafx.scene.paint.Color.web(mine ? "#0f766e" : "#cbd5e1")); if (message.getSenderAvatarUrl() != null && !message.getSenderAvatarUrl().isBlank()) { - try { - String fullUrl = org.example.petshopdesktop.api.ApiConfig.getInstance().getBaseUrl() + message.getSenderAvatarUrl(); - Image img = new Image(fullUrl, true); - img.errorProperty().addListener((obs, old, err) -> { - if (err) avatar.setFill(javafx.scene.paint.Color.web(mine ? "#0f766e" : "#cbd5e1")); - }); - avatar.setFill(new ImagePattern(img)); - } catch (Exception ignored) {} + String relativeUrl = message.getSenderAvatarUrl(); + new Thread(() -> { + try { + byte[] bytes = ApiClient.getInstance().getBytes(relativeUrl); + Image img = new Image(new java.io.ByteArrayInputStream(bytes)); + if (!img.isError()) { + Platform.runLater(() -> avatar.setFill(new ImagePattern(img))); + } + } catch (Exception ignored) {} + }).start(); } Label author = new Label(resolveAuthorLabel(message)); @@ -532,6 +534,12 @@ public class ChatController { content.setStyle("-fx-text-fill: " + (mine ? "#ffffff" : "#1f2937") + ";"); bubble.getChildren().add(content); } + if (message.getAttachmentName() != null && !message.getAttachmentName().isBlank()) { + Label attachmentLabel = new Label("\uD83D\uDCCE " + message.getAttachmentName()); + attachmentLabel.setStyle("-fx-text-fill: " + (mine ? "#dbeafe" : "#475569") + "; -fx-font-size: 12px;"); + attachmentLabel.setWrapText(true); + bubble.getChildren().add(attachmentLabel); + } bubble.getChildren().add(timestamp); bubble.setMaxWidth(420);