diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java index 69d5be52..0c3a51a0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java @@ -91,7 +91,15 @@ public class MessageAdapter extends RecyclerView.Adapter { @@ -109,7 +117,15 @@ public class MessageAdapter extends RecyclerView.Adapter { diff --git a/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java b/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java index 06cdf354..e5504a03 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java @@ -5,7 +5,6 @@ import com.example.petstoremobile.dtos.SendMessageRequest; import java.util.List; import okhttp3.MultipartBody; -import okhttp3.RequestBody; import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.http.Body; @@ -29,8 +28,8 @@ public interface MessageApi { @POST("api/v1/chat/conversations/{id}/attachments") Call sendMessageWithAttachment( @Path("id") Long conversationId, - @Part("content") RequestBody content, - @Part MultipartBody.Part attachment + @Part MultipartBody.Part content, + @Part MultipartBody.Part file ); @GET("api/v1/chat/messages/{id}/attachment") diff --git a/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java index 21ea11b8..4191de71 100644 --- a/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java +++ b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java @@ -55,7 +55,7 @@ public class NetworkModule { @Singleton public static OkHttpClient provideOkHttpClient(TokenManager tokenManager) { HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); - interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + interceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS); return new OkHttpClient.Builder() .addInterceptor(interceptor) @@ -191,4 +191,4 @@ public class NetworkModule { public static RefundApi provideRefundApi(Retrofit retrofit) { return retrofit.create(RefundApi.class); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index 7ad652ac..8ec8252a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -4,13 +4,11 @@ import android.app.Activity; import android.app.Dialog; import android.content.ContentValues; import android.content.Intent; -import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.provider.MediaStore; -import android.provider.OpenableColumns; import android.util.Log; import android.view.*; import android.view.inputmethod.EditorInfo; @@ -25,8 +23,6 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import com.bumptech.glide.Glide; -import com.bumptech.glide.load.model.GlideUrl; -import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ChatAdapter; import com.example.petstoremobile.adapters.MessageAdapter; @@ -37,8 +33,11 @@ import com.example.petstoremobile.dtos.MessageDTO; import com.example.petstoremobile.models.Chat; import com.example.petstoremobile.models.Message; import com.example.petstoremobile.services.ChatNotificationService; +import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.FileUtils; +import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.ChatListViewModel; import com.example.petstoremobile.websocket.StompChatManager; @@ -131,8 +130,8 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis binding.rvChatList.setAdapter(chatAdapter); messageAdapter = new MessageAdapter(messageList, null); - messageAdapter.setBaseUrl(baseUrl); - + messageAdapter.setBaseUrl(baseUrl); + messageAdapter.setOnAttachmentClickListener(message -> { if (message.getAttachmentMimeType() != null && message.getAttachmentMimeType().startsWith("image/")) { showFullScreenImage(message); @@ -153,22 +152,18 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis Dialog dialog = new Dialog(requireContext(), android.R.style.Theme_Black_NoTitleBar_Fullscreen); dialog.setContentView(R.layout.dialog_full_screen_image); - + ImageView imageView = dialog.findViewById(R.id.ivFullScreen); ImageButton closeButton = dialog.findViewById(R.id.btnClose); - + ImageButton downloadButton = dialog.findViewById(R.id.btnDownload); + String cleanBase = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; String downloadUrl = cleanBase + "/api/v1/chat/messages/" + message.getId() + "/attachment"; - GlideUrl glideUrl = new GlideUrl(downloadUrl, new LazyHeaders.Builder() - .addHeader("Authorization", "Bearer " + tokenManager.getToken()) - .build()); - - Glide.with(this) - .load(glideUrl) - .into(imageView); + GlideUtils.loadImageWithToken(requireContext(), imageView, downloadUrl, tokenManager.getToken(), R.drawable.placeholder); closeButton.setOnClickListener(v -> dialog.dismiss()); + downloadButton.setOnClickListener(v -> downloadFile(message)); imageView.setOnClickListener(v -> dialog.dismiss()); dialog.show(); } @@ -176,14 +171,17 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis private void downloadFile(Message message) { if (message.getId() == null) return; - Toast.makeText(requireContext(), "Downloading " + message.getAttachmentName() + "...", Toast.LENGTH_SHORT).show(); + DialogUtils.showConfirmDialog(requireContext(), "Download Attachment", + "Do you want to download \"" + message.getAttachmentName() + "\"?", () -> { + Toast.makeText(requireContext(), "Downloading " + message.getAttachmentName() + "...", Toast.LENGTH_SHORT).show(); - viewModel.downloadAttachment(message.getId()).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - saveFileToDownloads(resource.data, message.getAttachmentName(), message.getAttachmentMimeType()); - } else if (resource != null && resource.status == Resource.Status.ERROR) { - Toast.makeText(requireContext(), "Download failed: " + resource.message, Toast.LENGTH_SHORT).show(); - } + viewModel.downloadAttachment(message.getId()).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + saveFileToDownloads(resource.data, message.getAttachmentName(), message.getAttachmentMimeType()); + } else if (resource != null && resource.status == Resource.Status.ERROR) { + Toast.makeText(requireContext(), "Download failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); }); } @@ -220,7 +218,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis Toast.makeText(requireContext(), "File saved to Downloads: " + file.getAbsolutePath(), Toast.LENGTH_LONG).show(); } } - body.close(); + body.close(); } catch (Exception e) { Log.e(TAG, "Error saving file", e); Toast.makeText(requireContext(), "Error saving file", Toast.LENGTH_SHORT).show(); @@ -232,7 +230,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis chatList.clear(); chatList.addAll(list); chatAdapter.notifyDataSetChanged(); - + if (activeConversationId != null) { for (Chat chat : list) { if (chat.getChatId().equals(String.valueOf(activeConversationId))) { @@ -279,7 +277,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } viewModel.loadCustomers(); - + if (activeConversationId != null) { setConversationActive(true); if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId); @@ -346,12 +344,15 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } String text = binding.etMessage.getText().toString().trim(); - RequestBody contentPart = RequestBody.create(MediaType.parse("text/plain"), text); + + MultipartBody.Part contentPart = text.isEmpty() + ? null + : MultipartBody.Part.createFormData("content", text); String mimeType = requireContext().getContentResolver().getType(uri); if (mimeType == null) mimeType = "application/octet-stream"; - - RequestBody filePartBody = RequestBody.create(MediaType.parse(mimeType), file); + + RequestBody filePartBody = RequestBody.create(file, MediaType.parse(mimeType)); MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), filePartBody); binding.etMessage.setText(""); @@ -424,9 +425,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } private void setConversationActive(boolean active) { - binding.btnSend.setEnabled(active); - binding.etMessage.setEnabled(active); - binding.btnAttach.setEnabled(active); + UIUtils.setViewsEnabled(active, binding.btnSend, binding.etMessage, binding.btnAttach); if (!active) { activeConversationId = null; ChatNotificationService.activeConversationIdInUi = null; @@ -450,4 +449,4 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis ChatNotificationService.activeConversationIdInUi = null; if (stompChatManager != null) stompChatManager.disconnect(); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java index b962c9dc..bdc250c4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java @@ -62,7 +62,7 @@ public class ChatRepository extends BaseRepository { /** * Sends a message with an attachment. */ - public LiveData> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part attachment) { + public LiveData> sendMessageWithAttachment(Long conversationId, MultipartBody.Part content, MultipartBody.Part attachment) { return executeCall(messageApi.sendMessageWithAttachment(conversationId, content, attachment)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java index cac5636d..1dc5f716 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatListViewModel.java @@ -91,7 +91,7 @@ public class ChatListViewModel extends ViewModel { return chatRepository.sendMessage(conversationId, new SendMessageRequest(text)); } - public LiveData> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part attachment) { + public LiveData> sendMessageWithAttachment(Long conversationId, MultipartBody.Part content, MultipartBody.Part attachment) { return chatRepository.sendMessageWithAttachment(conversationId, content, attachment); } diff --git a/android/app/src/main/res/layout/dialog_full_screen_image.xml b/android/app/src/main/res/layout/dialog_full_screen_image.xml index c85b491e..0767baa2 100644 --- a/android/app/src/main/res/layout/dialog_full_screen_image.xml +++ b/android/app/src/main/res/layout/dialog_full_screen_image.xml @@ -10,15 +10,31 @@ android:layout_height="match_parent" android:scaleType="fitCenter" /> - + android:orientation="horizontal"> + + + + + \ No newline at end of file diff --git a/backend/src/main/java/com/petshop/backend/controller/ChatController.java b/backend/src/main/java/com/petshop/backend/controller/ChatController.java index 33e209c3..076f76cf 100644 --- a/backend/src/main/java/com/petshop/backend/controller/ChatController.java +++ b/backend/src/main/java/com/petshop/backend/controller/ChatController.java @@ -95,9 +95,10 @@ public class ChatController { @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity sendMessageWithAttachment( @PathVariable Long id, - @RequestParam("file") MultipartFile file) { + @RequestParam("file") MultipartFile file, + @RequestParam(value = "content", required = false) String content) { User user = getCurrentUser(); - MessageResponse message = chatService.sendMessageWithAttachment(id, user.getId(), user.getRole(), file); + MessageResponse message = chatService.sendMessageWithAttachment(id, user.getId(), user.getRole(), file, content); chatRealtimeService.publishMessage(id, message); chatRealtimeService.publishConversationUpdate(id); return ResponseEntity.status(HttpStatus.CREATED).body(message); diff --git a/backend/src/main/java/com/petshop/backend/service/ChatService.java b/backend/src/main/java/com/petshop/backend/service/ChatService.java index e17a5972..6ac092ce 100644 --- a/backend/src/main/java/com/petshop/backend/service/ChatService.java +++ b/backend/src/main/java/com/petshop/backend/service/ChatService.java @@ -151,7 +151,7 @@ public class ChatService { } @Transactional - public MessageResponse sendMessageWithAttachment(Long conversationId, Long userId, User.Role role, MultipartFile file) { + public MessageResponse sendMessageWithAttachment(Long conversationId, Long userId, User.Role role, MultipartFile file, String content) { Conversation conversation = conversationRepository.findById(conversationId) .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); @@ -173,6 +173,7 @@ public class ChatService { Message message = new Message(); message.setConversationId(conversationId); message.setSenderId(userId); + message.setContent(content); message.setAttachmentUrl(attachmentUrl); message.setAttachmentName(file.getOriginalFilename()); message.setAttachmentMimeType(file.getContentType()); diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 07b2c86b..0bc2169a 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -55,3 +55,9 @@ logging: com.petshop: ${LOG_LEVEL:INFO} org.springframework.security: ${LOG_LEVEL_SECURITY:WARN} org.springdoc.core.events.SpringDocAppInitializer: ERROR + + jackson: + serialization: + write-dates-as-timestamps: false + deserialization: + fail-on-unknown-properties: false \ No newline at end of file