fixed sending message with attachments

This commit is contained in:
Alex
2026-04-09 18:55:12 -06:00
parent 872e3a27f1
commit b9b74d2447
10 changed files with 90 additions and 52 deletions

View File

@@ -91,7 +91,15 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
this.binding = binding;
}
void bind(Message m, String token, String baseUrl, OnAttachmentClickListener listener) {
binding.tvMessageContent.setText(m.getContent());
// Check for Text
if (m.getContent() != null && !m.getContent().isEmpty()) {
binding.tvMessageContent.setVisibility(View.VISIBLE);
binding.tvMessageContent.setText(m.getContent());
} else {
binding.tvMessageContent.setVisibility(View.GONE);
}
// Check for Attachment
displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token, baseUrl);
View.OnClickListener click = v -> {
@@ -109,7 +117,15 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
this.binding = binding;
}
void bind(Message m, String token, String baseUrl, OnAttachmentClickListener listener) {
binding.tvMessageContent.setText(m.getContent());
// Check for Text
if (m.getContent() != null && !m.getContent().isEmpty()) {
binding.tvMessageContent.setVisibility(View.VISIBLE);
binding.tvMessageContent.setText(m.getContent());
} else {
binding.tvMessageContent.setVisibility(View.GONE);
}
// Check for Attachment
displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token, baseUrl);
View.OnClickListener click = v -> {

View File

@@ -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<MessageDTO> 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")

View File

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

View File

@@ -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;
@@ -156,19 +155,15 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
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();
@@ -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;

View File

@@ -62,7 +62,7 @@ public class ChatRepository extends BaseRepository {
/**
* Sends a message with an attachment.
*/
public LiveData<Resource<MessageDTO>> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part attachment) {
public LiveData<Resource<MessageDTO>> sendMessageWithAttachment(Long conversationId, MultipartBody.Part content, MultipartBody.Part attachment) {
return executeCall(messageApi.sendMessageWithAttachment(conversationId, content, attachment));
}

View File

@@ -91,7 +91,7 @@ public class ChatListViewModel extends ViewModel {
return chatRepository.sendMessage(conversationId, new SendMessageRequest(text));
}
public LiveData<Resource<MessageDTO>> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part attachment) {
public LiveData<Resource<MessageDTO>> sendMessageWithAttachment(Long conversationId, MultipartBody.Part content, MultipartBody.Part attachment) {
return chatRepository.sendMessageWithAttachment(conversationId, content, attachment);
}

View File

@@ -10,15 +10,31 @@
android:layout_height="match_parent"
android:scaleType="fitCenter" />
<ImageButton
android:id="@+id/btnClose"
android:layout_width="48dp"
android:layout_height="48dp"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_margin="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:contentDescription="Close"
android:tint="@android:color/white" />
android:orientation="horizontal">
<ImageButton
android:id="@+id/btnDownload"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@android:color/transparent"
android:src="@android:drawable/stat_sys_download"
android:contentDescription="Download"
android:tint="@android:color/white" />
<ImageButton
android:id="@+id/btnClose"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginLeft="8dp"
android:background="@android:color/transparent"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:contentDescription="Close"
android:tint="@android:color/white" />
</LinearLayout>
</FrameLayout>

View File

@@ -95,9 +95,10 @@ public class ChatController {
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<MessageResponse> 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);

View File

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

View File

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