Polish chat ui
This commit is contained in:
@@ -4,6 +4,7 @@ module org.example.petshopdesktop {
|
|||||||
requires javafx.web;
|
requires javafx.web;
|
||||||
requires java.sql;
|
requires java.sql;
|
||||||
requires java.net.http;
|
requires java.net.http;
|
||||||
|
requires java.prefs;
|
||||||
requires com.fasterxml.jackson.databind;
|
requires com.fasterxml.jackson.databind;
|
||||||
requires com.fasterxml.jackson.core;
|
requires com.fasterxml.jackson.core;
|
||||||
requires com.fasterxml.jackson.annotation;
|
requires com.fasterxml.jackson.annotation;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ public class ChatRealtimeClient implements WebSocket.Listener {
|
|||||||
private Consumer<ConversationResponse> conversationListener;
|
private Consumer<ConversationResponse> conversationListener;
|
||||||
private Consumer<MessageResponse> messageListener;
|
private Consumer<MessageResponse> messageListener;
|
||||||
private Consumer<String> statusListener;
|
private Consumer<String> statusListener;
|
||||||
|
private volatile String currentStatus = "Chat disconnected";
|
||||||
|
|
||||||
private ChatRealtimeClient() {
|
private ChatRealtimeClient() {
|
||||||
this.httpClient = HttpClient.newBuilder()
|
this.httpClient = HttpClient.newBuilder()
|
||||||
@@ -58,6 +59,9 @@ public class ChatRealtimeClient implements WebSocket.Listener {
|
|||||||
|
|
||||||
public void setStatusListener(Consumer<String> statusListener) {
|
public void setStatusListener(Consumer<String> statusListener) {
|
||||||
this.statusListener = statusListener;
|
this.statusListener = statusListener;
|
||||||
|
if (statusListener != null) {
|
||||||
|
statusListener.accept(currentStatus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void connect() {
|
public void connect() {
|
||||||
@@ -286,6 +290,7 @@ public class ChatRealtimeClient implements WebSocket.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void publishStatus(String status) {
|
private void publishStatus(String status) {
|
||||||
|
currentStatus = status;
|
||||||
if (statusListener != null) {
|
if (statusListener != null) {
|
||||||
statusListener.accept(status);
|
statusListener.accept(status);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import javafx.collections.ObservableList;
|
|||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TextInputDialog;
|
||||||
import javafx.scene.control.ListCell;
|
import javafx.scene.control.ListCell;
|
||||||
import javafx.scene.control.ListView;
|
import javafx.scene.control.ListView;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
@@ -31,6 +32,7 @@ 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.prefs.Preferences;
|
||||||
|
|
||||||
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");
|
||||||
@@ -59,6 +61,8 @@ public class ChatController {
|
|||||||
@FXML
|
@FXML
|
||||||
private Label lblChatStatus;
|
private Label lblChatStatus;
|
||||||
|
|
||||||
|
private static final Preferences CHAT_PREFERENCES = Preferences.userNodeForPackage(ChatController.class);
|
||||||
|
|
||||||
private final ObservableList<ConversationResponse> conversations = FXCollections.observableArrayList();
|
private final ObservableList<ConversationResponse> conversations = 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();
|
||||||
@@ -107,7 +111,7 @@ public class ChatController {
|
|||||||
realtimeClient.setConversationListener(conversation -> Platform.runLater(() -> upsertConversation(conversation)));
|
realtimeClient.setConversationListener(conversation -> Platform.runLater(() -> upsertConversation(conversation)));
|
||||||
realtimeClient.setMessageListener(message -> Platform.runLater(() -> appendMessageIfSelected(message)));
|
realtimeClient.setMessageListener(message -> Platform.runLater(() -> appendMessageIfSelected(message)));
|
||||||
realtimeClient.setStatusListener(status -> Platform.runLater(() -> lblChatStatus.setText(status)));
|
realtimeClient.setStatusListener(status -> Platform.runLater(() -> lblChatStatus.setText(status)));
|
||||||
realtimeClient.connect();
|
realtimeClient.subscribeToConversations();
|
||||||
|
|
||||||
loadCustomers();
|
loadCustomers();
|
||||||
loadConversations();
|
loadConversations();
|
||||||
@@ -121,6 +125,35 @@ public class ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void lblConversationTitleClicked() {
|
||||||
|
if (selectedConversation == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInputDialog dialog = new TextInputDialog(getConversationTitle(selectedConversation));
|
||||||
|
dialog.setTitle("Rename Conversation");
|
||||||
|
dialog.setHeaderText("Rename this conversation");
|
||||||
|
dialog.setContentText("Title:");
|
||||||
|
dialog.getEditor().setTextFormatter(new javafx.scene.control.TextFormatter<String>(change -> change.getControlNewText().length() <= 60 ? change : null));
|
||||||
|
|
||||||
|
Optional<String> result = dialog.showAndWait();
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String alias = result.get().trim();
|
||||||
|
String key = conversationPreferenceKey(selectedConversation.getId());
|
||||||
|
if (alias.isEmpty() || alias.equals(defaultConversationTitle(selectedConversation))) {
|
||||||
|
CHAT_PREFERENCES.remove(key);
|
||||||
|
} else {
|
||||||
|
CHAT_PREFERENCES.put(key, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
lblConversationTitle.setText(getConversationTitle(selectedConversation));
|
||||||
|
lvConversations.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
void btnSendClicked() {
|
void btnSendClicked() {
|
||||||
if (selectedConversation == null) {
|
if (selectedConversation == null) {
|
||||||
@@ -321,10 +354,19 @@ public class ChatController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getConversationTitle(ConversationResponse conversation) {
|
private String getConversationTitle(ConversationResponse conversation) {
|
||||||
|
String alias = CHAT_PREFERENCES.get(conversationPreferenceKey(conversation.getId()), null);
|
||||||
|
return alias != null && !alias.isBlank() ? alias : defaultConversationTitle(conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultConversationTitle(ConversationResponse conversation) {
|
||||||
String customerLabel = customerLabels.get(conversation.getCustomerId());
|
String customerLabel = customerLabels.get(conversation.getCustomerId());
|
||||||
return customerLabel != null ? customerLabel : "Customer #" + conversation.getCustomerId();
|
return customerLabel != null ? customerLabel : "Customer #" + conversation.getCustomerId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String conversationPreferenceKey(Long conversationId) {
|
||||||
|
return "chat.title." + conversationId;
|
||||||
|
}
|
||||||
|
|
||||||
private String buildConversationMeta(ConversationResponse conversation) {
|
private String buildConversationMeta(ConversationResponse conversation) {
|
||||||
String assignee;
|
String assignee;
|
||||||
if (conversation.getStaffId() != null) {
|
if (conversation.getStaffId() != null) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
<?import javafx.scene.text.Font?>
|
<?import javafx.scene.text.Font?>
|
||||||
|
|
||||||
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="742.0" prefWidth="1069.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.MainLayoutController">
|
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="742.0" prefWidth="1069.0" stylesheets="@styles/desktop-ui.css" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.MainLayoutController">
|
||||||
<left>
|
<left>
|
||||||
<VBox prefWidth="200.0" style="-fx-background-color: #2C3E50;" BorderPane.alignment="CENTER">
|
<VBox prefWidth="200.0" style="-fx-background-color: #2C3E50;" BorderPane.alignment="CENTER">
|
||||||
<children>
|
<children>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<Separator prefWidth="200.0" style="-fx-background-color: #444444; -fx-opacity: 0.35;" />
|
<Separator prefWidth="200.0" style="-fx-background-color: #444444; -fx-opacity: 0.35;" />
|
||||||
</children>
|
</children>
|
||||||
</VBox>
|
</VBox>
|
||||||
<ScrollPane fitToWidth="true" hbarPolicy="NEVER" style="-fx-background-color: #2C3E50; -fx-background: #2C3E50;" VBox.vgrow="ALWAYS">
|
<ScrollPane fitToWidth="true" hbarPolicy="NEVER" styleClass="sidebar-scroll-pane" style="-fx-background-color: #2C3E50; -fx-background: #2C3E50;" VBox.vgrow="ALWAYS">
|
||||||
<content>
|
<content>
|
||||||
<VBox spacing="6.0" style="-fx-background-color: #2C3E50;">
|
<VBox spacing="6.0" style="-fx-background-color: #2C3E50;">
|
||||||
<padding>
|
<padding>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
<?import javafx.scene.text.Font?>
|
<?import javafx.scene.text.Font?>
|
||||||
|
|
||||||
<VBox spacing="15.0" style="-fx-background-color: #f8fafc;" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.AnalyticsController">
|
<VBox spacing="15.0" style="-fx-background-color: #f8fafc;" stylesheets="@../styles/desktop-ui.css" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.AnalyticsController">
|
||||||
<padding>
|
<padding>
|
||||||
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
|
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
|
||||||
</padding>
|
</padding>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
</font>
|
</font>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<TabPane VBox.vgrow="ALWAYS">
|
<TabPane styleClass="analytics-tabs" VBox.vgrow="ALWAYS">
|
||||||
<Tab text="Overview" closable="false">
|
<Tab text="Overview" closable="false">
|
||||||
<VBox spacing="15.0">
|
<VBox spacing="15.0">
|
||||||
<padding>
|
<padding>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
<?import javafx.scene.text.Font?>
|
<?import javafx.scene.text.Font?>
|
||||||
|
|
||||||
<BorderPane prefHeight="680.0" prefWidth="900.0" style="-fx-background-color: linear-gradient(to bottom right, #f8fafc, #e2e8f0);" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.ChatController">
|
<BorderPane prefHeight="680.0" prefWidth="900.0" style="-fx-background-color: linear-gradient(to bottom right, #f8fafc, #e2e8f0);" stylesheets="@../styles/desktop-ui.css" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.ChatController">
|
||||||
<left>
|
<left>
|
||||||
<VBox prefWidth="290.0" spacing="12.0" style="-fx-background-color: #ffffff; -fx-border-color: #dbe4ee; -fx-border-width: 0 1 0 0;">
|
<VBox prefWidth="290.0" spacing="12.0" style="-fx-background-color: #ffffff; -fx-border-color: #dbe4ee; -fx-border-width: 0 1 0 0;">
|
||||||
<padding>
|
<padding>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
</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" style="-fx-background-color: transparent; -fx-border-color: transparent;" VBox.vgrow="ALWAYS" />
|
<ListView fx:id="lvConversations" prefHeight="620.0" styleClass="chat-conversation-list" style="-fx-background-color: transparent; -fx-border-color: transparent;" VBox.vgrow="ALWAYS" />
|
||||||
</children>
|
</children>
|
||||||
</VBox>
|
</VBox>
|
||||||
</left>
|
</left>
|
||||||
@@ -48,12 +48,12 @@
|
|||||||
<Insets bottom="18.0" left="18.0" right="18.0" top="18.0" />
|
<Insets bottom="18.0" left="18.0" right="18.0" top="18.0" />
|
||||||
</padding>
|
</padding>
|
||||||
<children>
|
<children>
|
||||||
<Label fx:id="lblConversationTitle" text="Select a conversation" textFill="#0f172a">
|
<Label fx:id="lblConversationTitle" onMouseClicked="#lblConversationTitleClicked" style="-fx-cursor: hand;" text="Select a conversation" textFill="#0f172a">
|
||||||
<font>
|
<font>
|
||||||
<Font name="System Bold" size="24.0" />
|
<Font name="System Bold" size="24.0" />
|
||||||
</font>
|
</font>
|
||||||
</Label>
|
</Label>
|
||||||
<ScrollPane fx:id="spMessages" fitToWidth="true" hbarPolicy="NEVER" style="-fx-background-color: transparent; -fx-background: transparent;" VBox.vgrow="ALWAYS">
|
<ScrollPane fx:id="spMessages" fitToWidth="true" hbarPolicy="NEVER" styleClass="chat-messages-scroll-pane" style="-fx-background-color: transparent; -fx-background: transparent;" VBox.vgrow="ALWAYS">
|
||||||
<content>
|
<content>
|
||||||
<VBox fx:id="vbMessages" spacing="10.0" style="-fx-background-color: rgba(255,255,255,0.72); -fx-background-radius: 18; -fx-padding: 18;" />
|
<VBox fx:id="vbMessages" spacing="10.0" style="-fx-background-color: rgba(255,255,255,0.72); -fx-background-radius: 18; -fx-padding: 18;" />
|
||||||
</content>
|
</content>
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
.sidebar-scroll-pane {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-padding: 0 4 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-scroll-pane > .viewport {
|
||||||
|
-fx-background-color: #2C3E50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-scroll-pane .scroll-bar:vertical,
|
||||||
|
.chat-conversation-list .scroll-bar:vertical,
|
||||||
|
.chat-messages-scroll-pane .scroll-bar:vertical {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-pref-width: 10;
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-scroll-pane .scroll-bar:vertical .track,
|
||||||
|
.chat-conversation-list .scroll-bar:vertical .track,
|
||||||
|
.chat-messages-scroll-pane .scroll-bar:vertical .track {
|
||||||
|
-fx-background-color: rgba(148, 163, 184, 0.18);
|
||||||
|
-fx-background-radius: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-scroll-pane .scroll-bar:vertical .thumb,
|
||||||
|
.chat-conversation-list .scroll-bar:vertical .thumb,
|
||||||
|
.chat-messages-scroll-pane .scroll-bar:vertical .thumb {
|
||||||
|
-fx-background-color: rgba(203, 213, 225, 0.52);
|
||||||
|
-fx-background-radius: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-scroll-pane .scroll-bar:vertical .increment-button,
|
||||||
|
.sidebar-scroll-pane .scroll-bar:vertical .decrement-button,
|
||||||
|
.chat-conversation-list .scroll-bar:vertical .increment-button,
|
||||||
|
.chat-conversation-list .scroll-bar:vertical .decrement-button,
|
||||||
|
.chat-messages-scroll-pane .scroll-bar:vertical .increment-button,
|
||||||
|
.chat-messages-scroll-pane .scroll-bar:vertical .decrement-button {
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-scroll-pane .scroll-bar:vertical .increment-arrow,
|
||||||
|
.sidebar-scroll-pane .scroll-bar:vertical .decrement-arrow,
|
||||||
|
.chat-conversation-list .scroll-bar:vertical .increment-arrow,
|
||||||
|
.chat-conversation-list .scroll-bar:vertical .decrement-arrow,
|
||||||
|
.chat-messages-scroll-pane .scroll-bar:vertical .increment-arrow,
|
||||||
|
.chat-messages-scroll-pane .scroll-bar:vertical .decrement-arrow {
|
||||||
|
-fx-shape: '';
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-tabs {
|
||||||
|
-fx-tab-min-height: 34;
|
||||||
|
-fx-tab-max-height: 34;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-tabs > .tab-header-area {
|
||||||
|
-fx-padding: 0 0 8 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-tabs > .tab-header-area > .headers-region > .tab {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-background-radius: 12 12 0 0;
|
||||||
|
-fx-border-color: transparent transparent rgba(148, 163, 184, 0.35) transparent;
|
||||||
|
-fx-border-width: 0 0 2 0;
|
||||||
|
-fx-padding: 8 16 8 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-tabs > .tab-header-area > .headers-region > .tab:selected {
|
||||||
|
-fx-background-color: white;
|
||||||
|
-fx-border-color: transparent transparent #ff6b6b transparent;
|
||||||
|
-fx-effect: dropshadow(gaussian, rgba(15, 23, 42, 0.08), 10, 0.2, 0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-tabs > .tab-header-area > .headers-region > .tab .tab-label {
|
||||||
|
-fx-text-fill: #64748b;
|
||||||
|
-fx-font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-tabs > .tab-header-area > .headers-region > .tab:selected .tab-label {
|
||||||
|
-fx-text-fill: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-tabs > .tab-header-area > .tab-header-background {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analytics-tabs > .tab-content-area {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-conversation-list {
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-conversation-list .list-cell {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-background-radius: 14;
|
||||||
|
-fx-padding: 10 10 10 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-conversation-list .list-cell:filled:selected,
|
||||||
|
.chat-conversation-list .list-cell:filled:hover {
|
||||||
|
-fx-background-color: #eef2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-conversation-list .list-cell:filled:selected {
|
||||||
|
-fx-border-color: #c7d2fe;
|
||||||
|
-fx-border-radius: 14;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages-scroll-pane,
|
||||||
|
.chat-messages-scroll-pane > .viewport {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user