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.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class ChatController {
|
public class ChatController {
|
||||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("MMM d, HH:mm");
|
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("MMM d, HH:mm");
|
||||||
|
private static final int CHAT_CELL_HEIGHT = 80;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private ListView<ConversationResponse> lvConversations;
|
private ListView<ConversationResponse> lvActiveConversations;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ListView<ConversationResponse> lvClosedConversations;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button btnActiveHeader;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button btnClosedHeader;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private VBox vbMessages;
|
private VBox vbMessages;
|
||||||
@@ -72,61 +83,49 @@ public class ChatController {
|
|||||||
private Label lblChatStatus;
|
private Label lblChatStatus;
|
||||||
|
|
||||||
private final ObservableList<ConversationResponse> conversations = FXCollections.observableArrayList();
|
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 Map<Long, String> customerLabels = new HashMap<>();
|
||||||
private final ChatRealtimeClient realtimeClient = ChatRealtimeClient.getInstance();
|
private final ChatRealtimeClient realtimeClient = ChatRealtimeClient.getInstance();
|
||||||
private ConversationResponse selectedConversation;
|
private ConversationResponse selectedConversation;
|
||||||
private File selectedAttachmentFile;
|
private File selectedAttachmentFile;
|
||||||
|
private boolean activeExpanded = true;
|
||||||
|
private boolean closedExpanded = true;
|
||||||
|
private boolean selectionChanging = false;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
lvConversations.setItems(conversations);
|
setupConversationListView(lvActiveConversations);
|
||||||
lvConversations.setCellFactory(list -> new ListCell<>() {
|
setupConversationListView(lvClosedConversations);
|
||||||
@Override
|
lvActiveConversations.setItems(activeConversations);
|
||||||
protected void updateItem(ConversationResponse item, boolean empty) {
|
lvClosedConversations.setItems(closedConversations);
|
||||||
super.updateItem(item, empty);
|
|
||||||
if (empty || item == null) {
|
|
||||||
setText(null);
|
|
||||||
setGraphic(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Label title = new Label(getConversationTitle(item));
|
lvActiveConversations.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> {
|
||||||
|
if (newVal != null && !selectionChanging) {
|
||||||
// Bold title if needs attention
|
selectionChanging = true;
|
||||||
UserSession session = UserSession.getInstance();
|
lvClosedConversations.getSelectionModel().clearSelection();
|
||||||
Long currentUserId = session.getUserId();
|
selectionChanging = false;
|
||||||
boolean needsPickup = item.getHumanRequestedAt() != null && item.getStaffId() == null;
|
onConversationSelected(newVal);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
lvConversations.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> {
|
lvClosedConversations.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> {
|
||||||
if (newValue != null) {
|
if (newVal != null && !selectionChanging) {
|
||||||
selectedConversation = newValue;
|
selectionChanging = true;
|
||||||
lblConversationTitle.setText(getConversationTitle(newValue));
|
lvActiveConversations.getSelectionModel().clearSelection();
|
||||||
loadMessages(newValue.getId());
|
selectionChanging = false;
|
||||||
realtimeClient.subscribeToConversation(newValue.getId());
|
onConversationSelected(newVal);
|
||||||
updateChatState(newValue);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
txtMessage.setOnKeyPressed(event -> {
|
txtMessage.setOnKeyPressed(event -> {
|
||||||
if (event.getCode() == KeyCode.ENTER && event.isControlDown()) {
|
if (event.getCode() == KeyCode.ENTER) {
|
||||||
btnSendClicked();
|
if (event.isShiftDown()) {
|
||||||
|
txtMessage.appendText("\n");
|
||||||
|
} else {
|
||||||
|
btnSendClicked();
|
||||||
|
}
|
||||||
|
event.consume();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,6 +144,22 @@ public class ChatController {
|
|||||||
updateChatState(null);
|
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
|
@FXML
|
||||||
void btnRefreshClicked() {
|
void btnRefreshClicked() {
|
||||||
loadConversations();
|
loadConversations();
|
||||||
@@ -241,6 +256,17 @@ public class ChatController {
|
|||||||
}).start();
|
}).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) {
|
private void updateChatState(ConversationResponse conv) {
|
||||||
boolean closed = conv == null || "CLOSED".equals(conv.getStatus());
|
boolean closed = conv == null || "CLOSED".equals(conv.getStatus());
|
||||||
txtMessage.setDisable(closed);
|
txtMessage.setDisable(closed);
|
||||||
@@ -248,6 +274,9 @@ public class ChatController {
|
|||||||
btnAttachment.setDisable(closed);
|
btnAttachment.setDisable(closed);
|
||||||
btnClose.setVisible(!closed);
|
btnClose.setVisible(!closed);
|
||||||
btnClose.setManaged(!closed);
|
btnClose.setManaged(!closed);
|
||||||
|
if (!closed) {
|
||||||
|
btnClose.setDisable(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearLocalAttachment() {
|
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;");
|
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() {
|
private void loadCustomers() {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
@@ -267,7 +359,8 @@ public class ChatController {
|
|||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
customerLabels.clear();
|
customerLabels.clear();
|
||||||
customerLabels.putAll(labels);
|
customerLabels.putAll(labels);
|
||||||
lvConversations.refresh();
|
lvActiveConversations.refresh();
|
||||||
|
lvClosedConversations.refresh();
|
||||||
if (selectedConversation != null) {
|
if (selectedConversation != null) {
|
||||||
lblConversationTitle.setText(getConversationTitle(selectedConversation));
|
lblConversationTitle.setText(getConversationTitle(selectedConversation));
|
||||||
}
|
}
|
||||||
@@ -288,9 +381,12 @@ public class ChatController {
|
|||||||
response.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder())));
|
response.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder())));
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
conversations.setAll(response);
|
conversations.setAll(response);
|
||||||
|
refreshSections();
|
||||||
restoreSelection();
|
restoreSelection();
|
||||||
if (selectedConversation == null && !conversations.isEmpty()) {
|
if (selectedConversation == null && !activeConversations.isEmpty()) {
|
||||||
lvConversations.getSelectionModel().selectFirst();
|
lvActiveConversations.getSelectionModel().selectFirst();
|
||||||
|
} else if (selectedConversation == null && !closedConversations.isEmpty()) {
|
||||||
|
lvClosedConversations.getSelectionModel().selectFirst();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -355,16 +451,15 @@ public class ChatController {
|
|||||||
.findFirst();
|
.findFirst();
|
||||||
|
|
||||||
if (existing.isPresent()) {
|
if (existing.isPresent()) {
|
||||||
ConversationResponse current = existing.get();
|
int index = conversations.indexOf(existing.get());
|
||||||
int index = conversations.indexOf(current);
|
|
||||||
conversations.set(index, conversation);
|
conversations.set(index, conversation);
|
||||||
} else {
|
} else {
|
||||||
conversations.add(conversation);
|
conversations.add(conversation);
|
||||||
}
|
}
|
||||||
|
|
||||||
conversations.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder())));
|
conversations.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder())));
|
||||||
|
refreshSections();
|
||||||
restoreSelection();
|
restoreSelection();
|
||||||
lvConversations.refresh();
|
|
||||||
if (selectedConversation != null && selectedConversation.getId().equals(conversation.getId())) {
|
if (selectedConversation != null && selectedConversation.getId().equals(conversation.getId())) {
|
||||||
updateChatState(conversation);
|
updateChatState(conversation);
|
||||||
}
|
}
|
||||||
@@ -378,7 +473,7 @@ public class ChatController {
|
|||||||
conversation.setLastMessage(message.getContent());
|
conversation.setLastMessage(message.getContent());
|
||||||
conversation.setUpdatedAt(message.getTimestamp());
|
conversation.setUpdatedAt(message.getTimestamp());
|
||||||
conversations.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder())));
|
conversations.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder())));
|
||||||
lvConversations.refresh();
|
refreshSections();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,12 +481,20 @@ public class ChatController {
|
|||||||
if (selectedConversation == null) {
|
if (selectedConversation == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
conversations.stream()
|
Long selId = selectedConversation.getId();
|
||||||
.filter(item -> item.getId().equals(selectedConversation.getId()))
|
activeConversations.stream()
|
||||||
|
.filter(item -> item.getId().equals(selId))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.ifPresent(match -> {
|
.ifPresent(match -> {
|
||||||
selectedConversation = 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>
|
</HBox>
|
||||||
<Label fx:id="lblChatStatus" text="Connecting chat..." textFill="#64748b" />
|
<Label fx:id="lblChatStatus" text="Connecting chat..." textFill="#64748b" />
|
||||||
<Separator />
|
<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>
|
</children>
|
||||||
</VBox>
|
</VBox>
|
||||||
</left>
|
</left>
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
-fx-background-color: transparent;
|
-fx-background-color: transparent;
|
||||||
-fx-background-radius: 14;
|
-fx-background-radius: 14;
|
||||||
-fx-padding: 10 10 10 10;
|
-fx-padding: 10 10 10 10;
|
||||||
|
-fx-pref-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-conversation-list .list-cell:filled:selected,
|
.chat-conversation-list .list-cell:filled:selected,
|
||||||
@@ -113,6 +114,16 @@
|
|||||||
-fx-background-insets: 0;
|
-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,
|
||||||
.chat-messages-scroll-pane > .viewport {
|
.chat-messages-scroll-pane > .viewport {
|
||||||
-fx-background-color: transparent;
|
-fx-background-color: transparent;
|
||||||
|
|||||||
Reference in New Issue
Block a user