Fix chat attachments and avatars

This commit is contained in:
2026-04-14 20:25:54 -06:00
parent ea1237942d
commit 8127b539e8
5 changed files with 68 additions and 22 deletions

View File

@@ -24,7 +24,7 @@ public class UserAvatarController {
}
@GetMapping("/{userId}/avatar/file")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<Resource> getUserAvatarFile(@PathVariable Long userId) {
User user = userRepository.findById(userId).orElse(null);
if (user == null || !avatarStorageService.hasAvatar(user)) {

View File

@@ -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) {

View File

@@ -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> T postMultipartWithText(String path, String filePartName, Path filePath,
String textPartName, String textContent,
Class<T> 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<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
return handleResponse(response, responseClass);
}
public <T> T put(String path, Object requestBody, Class<T> responseClass) throws Exception {
String jsonBody = objectMapper.writeValueAsString(requestBody);

View File

@@ -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);
}

View File

@@ -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);