Merge branch 'AttachmentsToChat'

This commit is contained in:
Alex
2026-04-14 23:43:39 -06:00
9 changed files with 118 additions and 18 deletions

View File

@@ -16,7 +16,10 @@ import com.example.petstoremobile.R;
import com.example.petstoremobile.databinding.ItemMessageReceivedBinding; import com.example.petstoremobile.databinding.ItemMessageReceivedBinding;
import com.example.petstoremobile.databinding.ItemMessageSentBinding; import com.example.petstoremobile.databinding.ItemMessageSentBinding;
import com.example.petstoremobile.models.Message; import com.example.petstoremobile.models.Message;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale;
public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
@@ -29,6 +32,7 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
private final List<Message> messages; private final List<Message> messages;
private Long currentUserId; private Long currentUserId;
private Long staffId;
private String token; private String token;
private String baseUrl; private String baseUrl;
private OnAttachmentClickListener attachmentClickListener; private OnAttachmentClickListener attachmentClickListener;
@@ -50,6 +54,11 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
notifyDataSetChanged(); notifyDataSetChanged();
} }
public void setStaffId(Long id) {
this.staffId = id;
notifyDataSetChanged();
}
public void setToken(String token) { public void setToken(String token) {
this.token = token; this.token = token;
} }
@@ -87,7 +96,7 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
Message m = messages.get(position); Message m = messages.get(position);
if (holder instanceof SentHolder) ((SentHolder) holder).bind(m, token, baseUrl, attachmentClickListener); if (holder instanceof SentHolder) ((SentHolder) holder).bind(m, token, baseUrl, attachmentClickListener);
if (holder instanceof ReceivedHolder) ((ReceivedHolder) holder).bind(m, token, baseUrl, attachmentClickListener); if (holder instanceof ReceivedHolder) ((ReceivedHolder) holder).bind(m, token, baseUrl, attachmentClickListener, staffId);
} }
@Override public int getItemCount() { return messages.size(); } @Override public int getItemCount() { return messages.size(); }
@@ -99,7 +108,9 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
this.binding = binding; this.binding = binding;
} }
void bind(Message m, String token, String baseUrl, OnAttachmentClickListener listener) { void bind(Message m, String token, String baseUrl, OnAttachmentClickListener listener) {
// Check for Text binding.tvSenderName.setText("You");
binding.tvTimestamp.setText(formatTimestamp(m.getTimestamp()));
if (m.getContent() != null && !m.getContent().isEmpty()) { if (m.getContent() != null && !m.getContent().isEmpty()) {
binding.tvMessageContent.setVisibility(View.VISIBLE); binding.tvMessageContent.setVisibility(View.VISIBLE);
binding.tvMessageContent.setText(m.getContent()); binding.tvMessageContent.setText(m.getContent());
@@ -107,7 +118,6 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
binding.tvMessageContent.setVisibility(View.GONE); binding.tvMessageContent.setVisibility(View.GONE);
} }
// Check for Attachment
displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token, baseUrl); displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token, baseUrl);
View.OnClickListener click = v -> { View.OnClickListener click = v -> {
@@ -124,8 +134,10 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
super(binding.getRoot()); super(binding.getRoot());
this.binding = binding; this.binding = binding;
} }
void bind(Message m, String token, String baseUrl, OnAttachmentClickListener listener) { void bind(Message m, String token, String baseUrl, OnAttachmentClickListener listener, Long staffId) {
// Check for Text binding.tvSenderName.setText(resolveSenderName(m, staffId));
binding.tvTimestamp.setText(formatTimestamp(m.getTimestamp()));
if (m.getContent() != null && !m.getContent().isEmpty()) { if (m.getContent() != null && !m.getContent().isEmpty()) {
binding.tvMessageContent.setVisibility(View.VISIBLE); binding.tvMessageContent.setVisibility(View.VISIBLE);
binding.tvMessageContent.setText(m.getContent()); binding.tvMessageContent.setText(m.getContent());
@@ -133,7 +145,6 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
binding.tvMessageContent.setVisibility(View.GONE); binding.tvMessageContent.setVisibility(View.GONE);
} }
// Check for Attachment
displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token, baseUrl); displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token, baseUrl);
View.OnClickListener click = v -> { View.OnClickListener click = v -> {
@@ -144,7 +155,29 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
} }
} }
// helper function to display the attachment to the chat bubble private static String resolveSenderName(Message m, Long staffId) {
if ("BOT".equalsIgnoreCase(m.getSenderRole())) {
return (m.getSenderDisplayName() != null && !m.getSenderDisplayName().isEmpty())
? m.getSenderDisplayName() : "AI Bot";
}
if (staffId != null && staffId.equals(m.getSenderId())) {
return "Staff";
}
return "Customer";
}
private static String formatTimestamp(String timestamp) {
if (timestamp == null || timestamp.isEmpty()) return "";
try {
String normalized = timestamp.length() > 19 ? timestamp.substring(0, 19) : timestamp;
SimpleDateFormat input = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
Date date = input.parse(normalized);
return new SimpleDateFormat("MMM d, HH:mm", Locale.getDefault()).format(date);
} catch (Exception e) {
return "";
}
}
private static void displayAttachment(Message m, ImageView iv, TextView tvName, String token, String baseUrl) { private static void displayAttachment(Message m, ImageView iv, TextView tvName, String token, String baseUrl) {
// Check if there's an attachment by looking at name or mime type // Check if there's an attachment by looking at name or mime type
if (m.getAttachmentName() != null || m.getAttachmentMimeType() != null) { if (m.getAttachmentName() != null || m.getAttachmentMimeType() != null) {

View File

@@ -34,6 +34,12 @@ public class MessageDTO {
@SerializedName("attachmentSizeBytes") @SerializedName("attachmentSizeBytes")
private Long attachmentSizeBytes; private Long attachmentSizeBytes;
@SerializedName("senderRole")
private String senderRole;
@SerializedName("senderDisplayName")
private String senderDisplayName;
public MessageDTO() {} public MessageDTO() {}
public Long getId() { return id; } public Long getId() { return id; }
@@ -65,4 +71,10 @@ public class MessageDTO {
public Long getAttachmentSizeBytes() { return attachmentSizeBytes; } public Long getAttachmentSizeBytes() { return attachmentSizeBytes; }
public void setAttachmentSizeBytes(Long attachmentSizeBytes) { this.attachmentSizeBytes = attachmentSizeBytes; } public void setAttachmentSizeBytes(Long attachmentSizeBytes) { this.attachmentSizeBytes = attachmentSizeBytes; }
public String getSenderRole() { return senderRole; }
public void setSenderRole(String senderRole) { this.senderRole = senderRole; }
public String getSenderDisplayName() { return senderDisplayName; }
public void setSenderDisplayName(String senderDisplayName) { this.senderDisplayName = senderDisplayName; }
} }

View File

@@ -362,6 +362,8 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
binding.tvChatTitle.setText(chat.getCustomerName()); binding.tvChatTitle.setText(chat.getCustomerName());
binding.chatDrawerLayout.closeDrawer(GravityCompat.START); binding.chatDrawerLayout.closeDrawer(GravityCompat.START);
messageAdapter.setStaffId(chat.getStaffId());
if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId); if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId);
viewModel.loadMessageHistory(activeConversationId); viewModel.loadMessageHistory(activeConversationId);
} }
@@ -470,10 +472,8 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
public void onMessageReceived(MessageDTO dto) { public void onMessageReceived(MessageDTO dto) {
requireActivity().runOnUiThread(() -> { requireActivity().runOnUiThread(() -> {
if (activeConversationId != null && activeConversationId.equals(dto.getConversationId())) { if (activeConversationId != null && activeConversationId.equals(dto.getConversationId())) {
if (!tokenManager.getUserId().equals(dto.getSenderId())) {
viewModel.addMessageLocally(dto); viewModel.addMessageLocally(dto);
} }
}
viewModel.updateConversationLocally(new ConversationDTO(dto.getConversationId(), 0L, 0L, dto.getContent(), "")); viewModel.updateConversationLocally(new ConversationDTO(dto.getConversationId(), 0L, 0L, dto.getContent(), ""));
}); });
} }

View File

@@ -11,6 +11,8 @@ public class Message {
private String attachmentName; private String attachmentName;
private String attachmentMimeType; private String attachmentMimeType;
private Long attachmentSizeBytes; private Long attachmentSizeBytes;
private String senderRole;
private String senderDisplayName;
public Message() {} public Message() {}
@@ -49,4 +51,10 @@ public class Message {
public Long getAttachmentSizeBytes() { return attachmentSizeBytes; } public Long getAttachmentSizeBytes() { return attachmentSizeBytes; }
public void setAttachmentSizeBytes(Long attachmentSizeBytes) { this.attachmentSizeBytes = attachmentSizeBytes; } public void setAttachmentSizeBytes(Long attachmentSizeBytes) { this.attachmentSizeBytes = attachmentSizeBytes; }
public String getSenderRole() { return senderRole; }
public void setSenderRole(String senderRole) { this.senderRole = senderRole; }
public String getSenderDisplayName() { return senderDisplayName; }
public void setSenderDisplayName(String senderDisplayName) { this.senderDisplayName = senderDisplayName; }
} }

View File

@@ -133,6 +133,11 @@ public class ChatListViewModel extends ViewModel {
public void addMessageLocally(MessageDTO dto) { public void addMessageLocally(MessageDTO dto) {
List<Message> current = new ArrayList<>(messageList.getValue()); List<Message> current = new ArrayList<>(messageList.getValue());
if (dto.getId() != null) {
for (Message m : current) {
if (dto.getId().equals(m.getId())) return;
}
}
current.add(dtoToModel(dto)); current.add(dtoToModel(dto));
messageList.setValue(current); messageList.setValue(current);
} }
@@ -168,6 +173,8 @@ public class ChatListViewModel extends ViewModel {
m.setIsRead(dto.getIsRead()); m.setIsRead(dto.getIsRead());
m.setAttachmentUrl(dto.getAttachmentUrl()); m.setAttachmentUrl(dto.getAttachmentUrl());
m.setAttachmentName(dto.getAttachmentName()); m.setAttachmentName(dto.getAttachmentName());
m.setSenderRole(dto.getSenderRole());
m.setSenderDisplayName(dto.getSenderDisplayName());
m.setAttachmentMimeType(dto.getAttachmentMimeType()); m.setAttachmentMimeType(dto.getAttachmentMimeType());
m.setAttachmentSizeBytes(dto.getAttachmentSizeBytes()); m.setAttachmentSizeBytes(dto.getAttachmentSizeBytes());
return m; return m;

View File

@@ -95,6 +95,7 @@ public class StompChatManager {
headers.put("Authorization", "Bearer " + authToken); headers.put("Authorization", "Bearer " + authToken);
stompClient = Stomp.over(Stomp.ConnectionProvider.OKHTTP, webSocketUrl, headers); stompClient = Stomp.over(Stomp.ConnectionProvider.OKHTTP, webSocketUrl, headers);
stompClient.withClientHeartbeat(0).withServerHeartbeat(0);
compositeDisposable.add( compositeDisposable.add(
stompClient.lifecycle() stompClient.lifecycle()

View File

@@ -14,6 +14,15 @@
android:padding="8dp" android:padding="8dp"
android:maxWidth="300dp"> android:maxWidth="300dp">
<TextView
android:id="@+id/tvSenderName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_dark"
android:textStyle="bold"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<ImageView <ImageView
android:id="@+id/ivAttachment" android:id="@+id/ivAttachment"
android:layout_width="200dp" android:layout_width="200dp"
@@ -38,6 +47,14 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Received message" android:text="Received message"
android:textColor="@color/text_dark" /> android:textColor="@color/text_dark" />
<TextView
android:id="@+id/tvTimestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#94a3b8"
android:textSize="11sp"
android:layout_marginTop="4dp"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -14,6 +14,16 @@
android:padding="8dp" android:padding="8dp"
android:maxWidth="300dp"> android:maxWidth="300dp">
<TextView
android:id="@+id/tvSenderName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="You"
android:textColor="@color/white"
android:textStyle="bold"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<ImageView <ImageView
android:id="@+id/ivAttachment" android:id="@+id/ivAttachment"
android:layout_width="200dp" android:layout_width="200dp"
@@ -38,6 +48,14 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Sent message" android:text="Sent message"
android:textColor="@color/white" /> android:textColor="@color/white" />
<TextView
android:id="@+id/tvTimestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#dbeafe"
android:textSize="11sp"
android:layout_marginTop="4dp"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -35,9 +35,11 @@ import java.io.File;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class ChatController { public class ChatController {
@@ -87,6 +89,7 @@ public class ChatController {
private final ObservableList<ConversationResponse> activeConversations = FXCollections.observableArrayList(); private final ObservableList<ConversationResponse> activeConversations = FXCollections.observableArrayList();
private final ObservableList<ConversationResponse> closedConversations = FXCollections.observableArrayList(); private final ObservableList<ConversationResponse> closedConversations = FXCollections.observableArrayList();
private final Map<Long, String> customerLabels = new HashMap<>(); private final Map<Long, String> customerLabels = new HashMap<>();
private final Set<Long> renderedMessageIds = new HashSet<>();
private final ChatRealtimeClient realtimeClient = ChatRealtimeClient.getInstance(); private final ChatRealtimeClient realtimeClient = ChatRealtimeClient.getInstance();
private ConversationResponse selectedConversation; private ConversationResponse selectedConversation;
private File selectedAttachmentFile; private File selectedAttachmentFile;
@@ -131,12 +134,7 @@ public class ChatController {
}); });
realtimeClient.setConversationListener(conversation -> Platform.runLater(() -> upsertConversation(conversation))); realtimeClient.setConversationListener(conversation -> Platform.runLater(() -> upsertConversation(conversation)));
realtimeClient.setMessageListener(message -> Platform.runLater(() -> { realtimeClient.setMessageListener(message -> Platform.runLater(() -> appendMessageIfSelected(message)));
Long myId = UserSession.getInstance().getUserId();
if (message.getSenderId() == null || !message.getSenderId().equals(myId)) {
appendMessageIfSelected(message);
}
}));
realtimeClient.setStatusListener(status -> Platform.runLater(() -> lblChatStatus.setText(status))); realtimeClient.setStatusListener(status -> Platform.runLater(() -> lblChatStatus.setText(status)));
realtimeClient.subscribeToConversations(); realtimeClient.subscribeToConversations();
@@ -419,9 +417,11 @@ public class ChatController {
private void renderMessages(List<MessageResponse> messages) { private void renderMessages(List<MessageResponse> messages) {
vbMessages.getChildren().clear(); vbMessages.getChildren().clear();
renderedMessageIds.clear();
for (MessageResponse message : messages) { for (MessageResponse message : messages) {
try { try {
vbMessages.getChildren().add(createMessageBubble(message)); vbMessages.getChildren().add(createMessageBubble(message));
if (message.getId() != null) renderedMessageIds.add(message.getId());
} catch (Exception e) { } catch (Exception e) {
ActivityLogger.getInstance().logException( ActivityLogger.getInstance().logException(
"ChatController.renderMessages", "ChatController.renderMessages",
@@ -436,7 +436,11 @@ public class ChatController {
try { try {
upsertConversationForMessage(message); upsertConversationForMessage(message);
if (selectedConversation != null && selectedConversation.getId().equals(message.getConversationId())) { if (selectedConversation != null && selectedConversation.getId().equals(message.getConversationId())) {
if (message.getId() != null && renderedMessageIds.contains(message.getId())) {
return;
}
vbMessages.getChildren().add(createMessageBubble(message)); vbMessages.getChildren().add(createMessageBubble(message));
if (message.getId() != null) renderedMessageIds.add(message.getId());
scrollMessagesToBottom(); scrollMessagesToBottom();
} }
} catch (Exception e) { } catch (Exception e) {