added closed chat section and fixed closed chat bug for desktop
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user