From 5f7f40f98a8fe77dada851bfd3535136e639b7c6 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:49:12 -0600 Subject: [PATCH] added closed chat section and fixed closed chat bug for desktop --- .../controllers/ChatController.java | 215 +++++++++++++----- .../petshopdesktop/modelviews/chat-view.fxml | 20 +- .../petshopdesktop/styles/desktop-ui.css | 11 + 3 files changed, 189 insertions(+), 57 deletions(-) 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 87f907d7..cd9c9c29 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java @@ -37,12 +37,23 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; public class ChatController { private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("MMM d, HH:mm"); + private static final int CHAT_CELL_HEIGHT = 80; @FXML - private ListView lvConversations; + private ListView lvActiveConversations; + + @FXML + private ListView lvClosedConversations; + + @FXML + private Button btnActiveHeader; + + @FXML + private Button btnClosedHeader; @FXML private VBox vbMessages; @@ -72,61 +83,49 @@ public class ChatController { private Label lblChatStatus; private final ObservableList conversations = FXCollections.observableArrayList(); + private final ObservableList activeConversations = FXCollections.observableArrayList(); + private final ObservableList closedConversations = FXCollections.observableArrayList(); private final Map customerLabels = new HashMap<>(); private final ChatRealtimeClient realtimeClient = ChatRealtimeClient.getInstance(); private ConversationResponse selectedConversation; private File selectedAttachmentFile; + private boolean activeExpanded = true; + private boolean closedExpanded = true; + private boolean selectionChanging = false; @FXML public void initialize() { - lvConversations.setItems(conversations); - lvConversations.setCellFactory(list -> new ListCell<>() { - @Override - protected void updateItem(ConversationResponse item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null) { - setText(null); - setGraphic(null); - return; - } + setupConversationListView(lvActiveConversations); + setupConversationListView(lvClosedConversations); + lvActiveConversations.setItems(activeConversations); + lvClosedConversations.setItems(closedConversations); - Label title = new Label(getConversationTitle(item)); - - // Bold title if needs attention - UserSession session = UserSession.getInstance(); - Long currentUserId = session.getUserId(); - boolean needsPickup = item.getHumanRequestedAt() != null && item.getStaffId() == null; - boolean needsReply = currentUserId != null && currentUserId.equals(item.getStaffId()) - && item.getLastSenderId() != null && !item.getLastSenderId().equals(currentUserId); - - if (needsPickup || needsReply) { - title.setStyle("-fx-font-weight: bold; -fx-text-fill: #FF6B6B;"); - } else { - title.setStyle("-fx-font-weight: bold; -fx-text-fill: #1f2937;"); - } - Label preview = new Label(item.getLastMessage() == null ? "" : item.getLastMessage()); - preview.setStyle("-fx-text-fill: #64748b;"); - preview.setWrapText(true); - Label meta = new Label(buildConversationMeta(item)); - meta.setStyle("-fx-text-fill: #94a3b8; -fx-font-size: 11px;"); - VBox box = new VBox(4, title, preview, meta); - setGraphic(box); + lvActiveConversations.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> { + if (newVal != null && !selectionChanging) { + selectionChanging = true; + lvClosedConversations.getSelectionModel().clearSelection(); + selectionChanging = false; + onConversationSelected(newVal); } }); - lvConversations.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> { - if (newValue != null) { - selectedConversation = newValue; - lblConversationTitle.setText(getConversationTitle(newValue)); - loadMessages(newValue.getId()); - realtimeClient.subscribeToConversation(newValue.getId()); - updateChatState(newValue); + lvClosedConversations.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> { + if (newVal != null && !selectionChanging) { + selectionChanging = true; + lvActiveConversations.getSelectionModel().clearSelection(); + selectionChanging = false; + onConversationSelected(newVal); } }); txtMessage.setOnKeyPressed(event -> { - if (event.getCode() == KeyCode.ENTER && event.isControlDown()) { - btnSendClicked(); + if (event.getCode() == KeyCode.ENTER) { + if (event.isShiftDown()) { + txtMessage.appendText("\n"); + } else { + btnSendClicked(); + } + event.consume(); } }); @@ -145,6 +144,22 @@ public class ChatController { updateChatState(null); } + @FXML + void toggleActiveSection() { + activeExpanded = !activeExpanded; + lvActiveConversations.setVisible(activeExpanded); + lvActiveConversations.setManaged(activeExpanded); + updateSectionHeaders(); + } + + @FXML + void toggleClosedSection() { + closedExpanded = !closedExpanded; + lvClosedConversations.setVisible(closedExpanded); + lvClosedConversations.setManaged(closedExpanded); + updateSectionHeaders(); + } + @FXML void btnRefreshClicked() { loadConversations(); @@ -169,12 +184,12 @@ public class ChatController { } Long convId = selectedConversation.getId(); - + txtMessage.clear(); btnSend.setDisable(true); - + lblChatStatus.setText("Sending message..."); - + new Thread(() -> { try { MessageRequest request = new MessageRequest(content); @@ -241,6 +256,17 @@ public class ChatController { }).start(); } + private void onConversationSelected(ConversationResponse newValue) { + boolean sameConversation = selectedConversation != null && selectedConversation.getId().equals(newValue.getId()); + selectedConversation = newValue; + lblConversationTitle.setText(getConversationTitle(newValue)); + if (!sameConversation) { + loadMessages(newValue.getId()); + realtimeClient.subscribeToConversation(newValue.getId()); + } + updateChatState(newValue); + } + private void updateChatState(ConversationResponse conv) { boolean closed = conv == null || "CLOSED".equals(conv.getStatus()); txtMessage.setDisable(closed); @@ -248,6 +274,9 @@ public class ChatController { btnAttachment.setDisable(closed); btnClose.setVisible(!closed); btnClose.setManaged(!closed); + if (!closed) { + btnClose.setDisable(false); + } } private void clearLocalAttachment() { @@ -256,6 +285,69 @@ public class ChatController { btnAttachment.setStyle("-fx-background-color: #e2e8f0; -fx-background-radius: 12; -fx-text-fill: #475569; -fx-cursor: hand;"); } + private void setupConversationListView(ListView lv) { + lv.setFixedCellSize(CHAT_CELL_HEIGHT); + lv.setCellFactory(list -> new ListCell<>() { + @Override + protected void updateItem(ConversationResponse item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setGraphic(null); + return; + } + + Label title = new Label(getConversationTitle(item)); + + UserSession session = UserSession.getInstance(); + Long currentUserId = session.getUserId(); + boolean needsPickup = item.getHumanRequestedAt() != null && item.getStaffId() == null; + boolean needsReply = currentUserId != null && currentUserId.equals(item.getStaffId()) + && item.getLastSenderId() != null && !item.getLastSenderId().equals(currentUserId); + + if (needsPickup || needsReply) { + title.setStyle("-fx-font-weight: bold; -fx-text-fill: #FF6B6B;"); + } else { + title.setStyle("-fx-font-weight: bold; -fx-text-fill: #1f2937;"); + } + Label preview = new Label(item.getLastMessage() == null ? "" : item.getLastMessage()); + preview.setStyle("-fx-text-fill: #64748b;"); + preview.setTextOverrun(javafx.scene.control.OverrunStyle.ELLIPSIS); + preview.setMaxWidth(Double.MAX_VALUE); + Label meta = new Label(buildConversationMeta(item)); + meta.setStyle("-fx-text-fill: #94a3b8; -fx-font-size: 11px;"); + VBox box = new VBox(4, title, preview, meta); + box.setMaxWidth(Double.MAX_VALUE); + setGraphic(box); + } + }); + } + + private void refreshSections() { + activeConversations.setAll(conversations.stream() + .filter(c -> !"CLOSED".equals(c.getStatus())) + .collect(Collectors.toList())); + closedConversations.setAll(conversations.stream() + .filter(c -> "CLOSED".equals(c.getStatus())) + .collect(Collectors.toList())); + updateSectionHeaders(); + updateListViewHeights(); + } + + private void updateSectionHeaders() { + btnActiveHeader.setText((activeExpanded ? "▼" : "▶") + " Active Chats (" + activeConversations.size() + ")"); + btnClosedHeader.setText((closedExpanded ? "▼" : "▶") + " Closed Chats (" + closedConversations.size() + ")"); + } + + private void updateListViewHeights() { + if (activeExpanded) { + lvActiveConversations.setPrefHeight(activeConversations.size() * CHAT_CELL_HEIGHT + 2); + } + if (closedExpanded) { + lvClosedConversations.setPrefHeight(closedConversations.size() * CHAT_CELL_HEIGHT + 2); + } + } + private void loadCustomers() { new Thread(() -> { try { @@ -267,7 +359,8 @@ public class ChatController { Platform.runLater(() -> { customerLabels.clear(); customerLabels.putAll(labels); - lvConversations.refresh(); + lvActiveConversations.refresh(); + lvClosedConversations.refresh(); if (selectedConversation != null) { lblConversationTitle.setText(getConversationTitle(selectedConversation)); } @@ -288,9 +381,12 @@ public class ChatController { response.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder()))); Platform.runLater(() -> { conversations.setAll(response); + refreshSections(); restoreSelection(); - if (selectedConversation == null && !conversations.isEmpty()) { - lvConversations.getSelectionModel().selectFirst(); + if (selectedConversation == null && !activeConversations.isEmpty()) { + lvActiveConversations.getSelectionModel().selectFirst(); + } else if (selectedConversation == null && !closedConversations.isEmpty()) { + lvClosedConversations.getSelectionModel().selectFirst(); } }); } catch (Exception e) { @@ -355,16 +451,15 @@ public class ChatController { .findFirst(); if (existing.isPresent()) { - ConversationResponse current = existing.get(); - int index = conversations.indexOf(current); + int index = conversations.indexOf(existing.get()); conversations.set(index, conversation); } else { conversations.add(conversation); } conversations.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder()))); + refreshSections(); restoreSelection(); - lvConversations.refresh(); if (selectedConversation != null && selectedConversation.getId().equals(conversation.getId())) { updateChatState(conversation); } @@ -378,7 +473,7 @@ public class ChatController { conversation.setLastMessage(message.getContent()); conversation.setUpdatedAt(message.getTimestamp()); conversations.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder()))); - lvConversations.refresh(); + refreshSections(); }); } @@ -386,18 +481,26 @@ public class ChatController { if (selectedConversation == null) { return; } - conversations.stream() - .filter(item -> item.getId().equals(selectedConversation.getId())) + Long selId = selectedConversation.getId(); + activeConversations.stream() + .filter(item -> item.getId().equals(selId)) .findFirst() .ifPresent(match -> { selectedConversation = match; - lvConversations.getSelectionModel().select(match); + lvActiveConversations.getSelectionModel().select(match); + }); + closedConversations.stream() + .filter(item -> item.getId().equals(selId)) + .findFirst() + .ifPresent(match -> { + selectedConversation = match; + lvClosedConversations.getSelectionModel().select(match); }); } private HBox createMessageBubble(MessageResponse message) { boolean mine = message.getSenderId() != null && message.getSenderId().equals(UserSession.getInstance().getUserId()); - + Circle avatar = new Circle(16); avatar.setFill(javafx.scene.paint.Color.web(mine ? "#0f766e" : "#cbd5e1")); if (message.getSenderAvatarUrl() != null && !message.getSenderAvatarUrl().isBlank()) { @@ -437,7 +540,7 @@ public class ChatController { HBox.setHgrow(spacer, Priority.ALWAYS); HBox container = new HBox(12); container.setAlignment(javafx.geometry.Pos.BOTTOM_LEFT); - + if (mine) { container.getChildren().addAll(spacer, bubble, avatar); container.setAlignment(javafx.geometry.Pos.BOTTOM_RIGHT); diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/chat-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/chat-view.fxml index c1294eab..e3b1dbd4 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/chat-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/chat-view.fxml @@ -38,7 +38,25 @@