added closed chat section and fixed closed chat bug for desktop

This commit is contained in:
Alex
2026-04-12 19:49:12 -06:00
parent d870475bc9
commit 5f7f40f98a
3 changed files with 189 additions and 57 deletions

View File

@@ -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<ConversationResponse> lvConversations;
private ListView<ConversationResponse> lvActiveConversations;
@FXML
private ListView<ConversationResponse> 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<ConversationResponse> conversations = FXCollections.observableArrayList();
private final ObservableList<ConversationResponse> activeConversations = FXCollections.observableArrayList();
private final ObservableList<ConversationResponse> closedConversations = FXCollections.observableArrayList();
private final Map<Long, String> 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();
@@ -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<ConversationResponse> 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,12 +481,20 @@ 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);
});
}

View File

@@ -38,7 +38,25 @@
</HBox>
<Label fx:id="lblChatStatus" text="Connecting chat..." textFill="#64748b" />
<Separator />
<ListView fx:id="lvConversations" prefHeight="620.0" styleClass="chat-conversation-list" style="-fx-background-color: transparent; -fx-border-color: transparent;" VBox.vgrow="ALWAYS" />
<ScrollPane VBox.vgrow="ALWAYS" fitToWidth="true" hbarPolicy="NEVER" style="-fx-background: transparent; -fx-background-color: transparent; -fx-border-color: transparent;">
<content>
<VBox spacing="4.0">
<Button fx:id="btnActiveHeader" maxWidth="Infinity" alignment="CENTER_LEFT" mnemonicParsing="false" onAction="#toggleActiveSection" style="-fx-background-color: #f1f5f9; -fx-cursor: hand; -fx-background-radius: 8; -fx-font-weight: bold; -fx-text-fill: #374151;" text="▼ Active Chats (0)">
<padding>
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
</padding>
</Button>
<ListView fx:id="lvActiveConversations" styleClass="chat-conversation-list" style="-fx-background-color: transparent; -fx-border-color: transparent;" />
<Separator />
<Button fx:id="btnClosedHeader" maxWidth="Infinity" alignment="CENTER_LEFT" mnemonicParsing="false" onAction="#toggleClosedSection" style="-fx-background-color: #f1f5f9; -fx-cursor: hand; -fx-background-radius: 8; -fx-font-weight: bold; -fx-text-fill: #374151;" text="▼ Closed Chats (0)">
<padding>
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
</padding>
</Button>
<ListView fx:id="lvClosedConversations" styleClass="chat-conversation-list" style="-fx-background-color: transparent; -fx-border-color: transparent;" />
</VBox>
</content>
</ScrollPane>
</children>
</VBox>
</left>

View File

@@ -100,6 +100,7 @@
-fx-background-color: transparent;
-fx-background-radius: 14;
-fx-padding: 10 10 10 10;
-fx-pref-width: 0;
}
.chat-conversation-list .list-cell:filled:selected,
@@ -113,6 +114,16 @@
-fx-background-insets: 0;
}
.chat-conversation-list .scroll-bar {
-fx-pref-width: 0;
-fx-pref-height: 0;
visibility: hidden;
}
.chat-conversation-list .corner {
visibility: hidden;
}
.chat-messages-scroll-pane,
.chat-messages-scroll-pane > .viewport {
-fx-background-color: transparent;