Typing indicator + Metrics

This commit is contained in:
2023-12-24 02:46:27 +02:00
parent ac58009119
commit a477a87c3d
20 changed files with 1201 additions and 316 deletions
@@ -2,12 +2,15 @@ package dev.wiing.gossip.client;
import dev.wiing.gossip.lib.PacketHandler;
import dev.wiing.gossip.lib.PacketManager;
import dev.wiing.gossip.lib.PacketMetrics;
import dev.wiing.gossip.lib.data.LongData;
import dev.wiing.gossip.lib.models.SecretUser;
import dev.wiing.gossip.lib.models.Topic;
import dev.wiing.gossip.lib.models.User;
import dev.wiing.gossip.lib.packets.*;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
@@ -18,6 +21,13 @@ public class Connection {
private static final Connection instance = new Connection();
private PropertyChangeSupport changeSupport;
private boolean recordStats = false;
private String compiledConnectionStats = "CONNECTION STATS\nNeed an update";
private String compiledCacheStats = "CACHE STATS\nNeed an update";
private final PacketManager packetManager = new PacketManager();
private final PacketHandler packetHandler = new PacketHandler();
@@ -32,6 +42,7 @@ public class Connection {
private Connection() {
packetManager.registerPackets();
changeSupport = new PropertyChangeSupport(this);
}
public static Connection getInstance() {
@@ -51,6 +62,8 @@ public class Connection {
public PacketHandlerRunnable(Connection connection) {
this.connection = connection;
connection.packetHandler.addAllPacketsToMap(connection.packetManager.getPacketMap().values());
}
@Override
@@ -69,6 +82,8 @@ public class Connection {
if (!connection.getPacketHandler().runPacket(packet)) {
connection.queuedPackets.add(packet);
}
connection.updateConnectionStats();
} catch (SocketException e) {
break;
} catch (IOException e) {
@@ -83,6 +98,91 @@ public class Connection {
handlerThread.start();
}
public void updateConnectionStats() {
if (!recordStats) return;
StringBuilder sb = new StringBuilder();
PacketMetrics m = packetManager.getPacketMetrics();
sb.append("OUTGOING CONNECTION\n");
sb.append("Packets Sent: ").append(m.getTotalPacketsSentCount()).append("\n");
sb.append("Bytes Sent: ").append(m.getTotalPacketsSentBytes()).append(" B\n");
sb.append("Common Types:\n");
m.getSendHistory().values().stream()
.filter(packets -> !packets.isEmpty())
.sorted((o1, o2) -> Integer.compare(o2.size(), o1.size()))
.limit(4)
.forEachOrdered(packets -> {
sb .append(" ")
.append(packets.size())
.append("x ")
.append(packets.get(0).getClass().getSimpleName())
.append("\n");
});
sb.append("\n");
sb.append("INCOMING CONNECTION\n");
sb.append("Packets Received: ").append(m.getTotalPacketsReceivedCount()).append("\n");
sb.append("Packets in Queue: ").append(getQueueSize()).append("\n");
sb.append("Bytes Received: ").append(m.getTotalPacketsReceivedBytes()).append(" B\n");
sb.append("Common Types:\n");
m.getReceiveHistory().values().stream()
.filter(packets -> !packets.isEmpty())
.sorted((o1, o2) -> Integer.compare(o2.size(), o1.size()))
.limit(4)
.forEachOrdered(packets -> {
sb .append(" ")
.append(packets.size())
.append("x ")
.append(packets.get(0).getClass().getSimpleName())
.append("\n");
});
compiledConnectionStats = sb.toString();
changeSupport.firePropertyChange("connectionStatsChange", null, compiledConnectionStats);
}
public void updateCacheStats() {
if (!recordStats) return;
StringBuilder sb = new StringBuilder();
sb.append("DATA CACHE\n");
sb.append("Users Cached: ").append(userCache.getSize()).append("\n");
sb.append("Topics Cached: ").append(topicCache.getSize()).append("\n");
compiledCacheStats = sb.toString();
changeSupport.firePropertyChange("cacheStatsChange", null, compiledCacheStats);
}
public boolean isRecordStats() {
return recordStats;
}
public String getCompiledConnectionStats() {
return compiledConnectionStats;
}
public String getCompiledCacheStats() {
return compiledCacheStats;
}
public void setRecordStats(boolean recordStats) {
this.recordStats = recordStats;
}
public void addStatListener(PropertyChangeListener listener) {
changeSupport.addPropertyChangeListener(listener);
}
public void removeStatListener(PropertyChangeListener listener) {
changeSupport.removePropertyChangeListener(listener);
}
public Socket getSocket() {
return socket;
}
@@ -134,6 +234,8 @@ public class Connection {
continue;
}
updateConnectionStats();
return packet;
}
}
@@ -192,6 +294,10 @@ public class Connection {
}
}
public int getQueueSize() {
return queuedPackets.size();
}
public SecretUser getSelf() {
return self;
}
@@ -230,7 +336,20 @@ public class Connection {
});
public User getUser(long userID) {
return userCache.get(userID);
int prevSize = userCache.getSize();
User result = userCache.get(userID);
if (prevSize != userCache.getSize())
updateCacheStats();
return result;
}
public void saveUser(User user) {
userCache.put(user.getUserID(), user);
updateCacheStats();
}
private final DataCache<Topic> topicCache = new DataCache<>(id -> {
@@ -263,6 +382,19 @@ public class Connection {
});
public Topic getTopic(long topicID) {
return topicCache.get(topicID);
int prevSize = topicCache.getSize();
Topic result = topicCache.get(topicID);
if (prevSize != topicCache.getSize())
updateCacheStats();
return result;
}
public void saveTopic(Topic topic) {
topicCache.put(topic.getId(), topic);
updateCacheStats();
}
}
@@ -30,13 +30,15 @@ public class DataCache<T> {
T result;
if ((result = cache.getOrDefault(id, null)) != null) {
return result;
}
synchronized (cache) {
if ((result = cache.getOrDefault(id, null)) != null) {
return result;
}
result = fetchFunction.fetch(id);
if (result != null) {
cache.put(id, result);
result = fetchFunction.fetch(id);
if (result != null) {
cache.put(id, result);
}
}
return result;
@@ -45,4 +47,8 @@ public class DataCache<T> {
public void put(long id, T val) {
cache.put(id, val);
}
public int getSize() {
return cache.size();
}
}
@@ -0,0 +1,120 @@
package dev.wiing.gossip.client.controllers;
import dev.wiing.gossip.client.data.UserAvatar;
import dev.wiing.gossip.client.generic.Pair;
import dev.wiing.gossip.client.utils.Utils;
import dev.wiing.gossip.lib.models.User;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.effect.Effect;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import java.net.URL;
import java.util.ResourceBundle;
public class AvatarController implements Initializable {
private VBox selectedAvatarPair;
private UserAvatar selectedAvatar;
@FXML
private FlowPane flowAvatars;
@FXML
private Label lblSelectedDesc;
@FXML
private Label lblSelectedName;
@FXML
private Pane paneSelectedIcon;
@FXML
private VBox vboxAvatarButton;
@FXML
private Button btnSave;
@FXML
private VBox vboxRoot;
@Override
public void initialize(URL location, ResourceBundle resources) {
flowAvatars.getChildren().clear();
UserAvatar.MORNING.applyToRegionBackground(paneSelectedIcon, true);
Effect effect = new ColorAdjust(0.0, -1.0, -0.4, 0.0);
for (UserAvatar avatar : UserAvatar.getAvatars()) {
VBox vBox = new VBox();
vBox.getStyleClass().add("clickable");
vBox.setSpacing(16);
vBox.setAlignment(Pos.CENTER);
Pane pane = new Pane();
pane.setMinSize(64, 64);
pane.setMaxSize(64, 64);
avatar.applyToRegionBackground(pane, true);
Label label = new Label();
label.setText(avatar.getName());
label.getStyleClass().addAll("axis", "accent");
vBox.getChildren().addAll(pane, label);
vBox.setEffect(effect);
vBox.setOnMouseClicked(event -> {
if (selectedAvatarPair != null) {
selectedAvatarPair.setEffect(effect);
}
selectedAvatarPair = vBox;
selectAvatar(avatar);
vBox.setEffect(null);
});
flowAvatars.getChildren().add(vBox);
}
}
public void selectAvatar(UserAvatar avatar) {
selectedAvatar = avatar;
lblSelectedName.setText(avatar.getName());
lblSelectedDesc.setText(avatar.getDescription());
avatar.applyToRegionBackground(paneSelectedIcon, false);
vboxRoot.setStyle("-accent: " + selectedAvatar.getColor());
}
public UserAvatar getSelectedAvatar() {
return selectedAvatar;
}
public void setOnSave(EventHandler<ActionEvent> eventHandler) {
btnSave.setOnAction(event -> {
btnSave.getScene().getWindow().hide();
eventHandler.handle(event);
});
}
public static Pair<Parent, AvatarController> createInstance() {
return Utils.createInstance("views/avatar-view.fxml");
}
}
@@ -3,6 +3,7 @@ package dev.wiing.gossip.client.controllers;
import dev.wiing.gossip.client.Connection;
import dev.wiing.gossip.client.data.UserAvatar;
import dev.wiing.gossip.client.utils.Utils;
import dev.wiing.gossip.lib.models.SecretUser;
import dev.wiing.gossip.lib.packets.RegisterCredentialsPacket;
import dev.wiing.gossip.lib.packets.Packet;
@@ -35,6 +36,11 @@ public class LoginController implements Initializable {
@FXML
private TextField txtUsername;
@FXML
private VBox vboxRoot;
private UserAvatar selectedAvatar = UserAvatar.MORNING;
@Override
public void initialize(URL location, ResourceBundle resources) {
UserAvatar.MORNING.applyToRegionBackground(paneIcon, false);
@@ -42,17 +48,31 @@ public class LoginController implements Initializable {
Circle circle = new Circle(paneIcon.getMinWidth() * 0.5);
paneIcon.setShape(circle);
hboxEditOverlay.setShape(circle);
vboxRoot.setStyle("-accent: " + selectedAvatar.getColor());
}
@FXML
public void onChangeIcon(MouseEvent event) {
var pair = AvatarController.createInstance();
pair.second().selectAvatar(selectedAvatar);
Utils.openParentAsWindow(pair.first(), "Gossip -- Avatar Menu");
pair.second().setOnSave(event1 -> {
selectedAvatar = pair.second().getSelectedAvatar();
selectedAvatar.applyToRegionBackground(paneIcon, false);
vboxRoot.setStyle("-accent: " + selectedAvatar.getColor());
});
}
@FXML
public void onLogin(ActionEvent event) {
RegisterRequestPacket packet = new RegisterRequestPacket();
packet.setAvatarID((byte)0);
packet.setAvatarID((byte)selectedAvatar.getId());
packet.setUsername(txtUsername.getText());
Connection.getInstance().sendPacket(packet);
@@ -6,17 +6,22 @@ import dev.wiing.gossip.client.controllers.item.SystemMessageItemController;
import dev.wiing.gossip.client.generic.Pair;
import dev.wiing.gossip.client.utils.Utils;
import dev.wiing.gossip.lib.models.SystemMessage;
import dev.wiing.gossip.lib.models.User;
import dev.wiing.gossip.lib.models.UserMessage;
import dev.wiing.gossip.lib.models.Topic;
import dev.wiing.gossip.lib.packets.MessagePushPacket;
import dev.wiing.gossip.lib.packets.TopicJoinPacket;
import dev.wiing.gossip.lib.packets.TypingPingPacket;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.beans.PropertyChangeEvent;
@@ -64,6 +69,10 @@ public class MainChatController {
@FXML
private VBox vboxVisitPage;
private Thread typingWaitThread = null;
private boolean composeFieldPreviouslyEmpty = true;
public void setTopic(Topic topic) {
this.topic = topic;
@@ -82,7 +91,7 @@ public class MainChatController {
});
}
if (evt.getPropertyName().equals("messageAdd")) {
else if (evt.getPropertyName().equals("messageAdd")) {
Platform.runLater(() -> {
if (evt.getNewValue() instanceof UserMessage) {
addMessage((UserMessage)evt.getNewValue());
@@ -93,7 +102,13 @@ public class MainChatController {
}
});
}
else if (evt.getPropertyName().equals("typingAdd") || evt.getPropertyName().equals("typingRemove")) {
Platform.runLater(this::updateTypingMembers);
}
});
updateTypingMembers();
}
private void setTopicName(String name) {
@@ -163,24 +178,111 @@ public class MainChatController {
}
}
@FXML
void onSend(MouseEvent event) {
private void sendMessage() {
String messageContents = txtCompose.getText();
if (messageContents.isBlank()) {
return;
}
composeFieldPreviouslyEmpty = true;
txtCompose.clear();
MessagePushPacket packet = new MessagePushPacket();
packet.setTopicID(topic.getId());
packet.setMessage(messageContents);
packet.setMessage(messageContents.strip());
Connection.getInstance().sendPacketAuthenticated(packet);
try {
Connection.getInstance().findAck(MessagePushPacket.TYPE);
} catch (IOException e) {
throw new RuntimeException(e);
}
TypingPingPacket pingPacket = new TypingPingPacket();
pingPacket.setTyping(false);
pingPacket.setTopicID(topic.getId());
Connection.getInstance().sendPacketAuthenticated(pingPacket);
typingWaitThread.interrupt();
typingWaitThread = null;
}
@FXML
void onSend(MouseEvent event) {
sendMessage();
}
@FXML
void onKeyTyped(KeyEvent event) {
boolean isBlankTextField = txtCompose.getText().isBlank();
if (!isBlankTextField && typingWaitThread != null && typingWaitThread.isAlive()) return;
if (isBlankTextField && composeFieldPreviouslyEmpty) return;
TypingPingPacket packet = new TypingPingPacket();
packet.setTyping(!isBlankTextField);
packet.setTopicID(topic.getId());
Connection.getInstance().sendPacketAuthenticated(packet);
composeFieldPreviouslyEmpty = isBlankTextField;
if (typingWaitThread != null) typingWaitThread.interrupt();
if (isBlankTextField) return;
typingWaitThread = new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException ignored) {
}
});
typingWaitThread.start();
}
@FXML
void onKeyPressed(KeyEvent event) {
if (event.getCode() == KeyCode.ENTER) {
if (event.isShiftDown()) {
txtCompose.insertText(txtCompose.getLength(), "\n");
} else {
sendMessage();
}
}
}
public void updateTypingMembers() {
if (topic.getTypingUsersCount() == 0) {
lblTyping.setText("");
lblTyping.setMinHeight(0);
lblTyping.setMaxHeight(0);
return;
}
lblTyping.setMinHeight(Region.USE_COMPUTED_SIZE);
lblTyping.setMaxHeight(Region.USE_COMPUTED_SIZE);
if (topic.getTypingUsersCount() > 3) {
lblTyping.setText("Several babblers are writing...");
return;
}
StringBuilder sb = new StringBuilder();
for (User user : topic.getTypingUsersReadOnly()) {
if (!sb.isEmpty()) sb.append(", ");
sb.append(user.getUsername());
}
sb.append(topic.getTypingUsersCount() == 1 ? " is" : " are");
sb.append(" writing...");
lblTyping.setText(sb.toString());
}
public static Pair<Parent, MainChatController> createInstance() {
@@ -188,3 +290,6 @@ public class MainChatController {
}
}
// TODO: better mutliline input
// 25 + 17 per extra line
@@ -5,6 +5,7 @@ import dev.wiing.gossip.client.controllers.item.TopicItemController;
import dev.wiing.gossip.client.data.UserAvatar;
import dev.wiing.gossip.client.generic.Pair;
import dev.wiing.gossip.client.utils.Utils;
import dev.wiing.gossip.lib.PacketHandler;
import dev.wiing.gossip.lib.data.LongData;
import dev.wiing.gossip.lib.models.*;
import dev.wiing.gossip.lib.packets.*;
@@ -14,15 +15,16 @@ import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import java.net.SocketException;
import java.net.URL;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public class MainController implements Initializable {
@@ -48,6 +50,11 @@ public class MainController implements Initializable {
@FXML
private Label lblJoinMessage;
@FXML
private Label lblDebug;
private boolean recordDebugStats = false;
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
User user = Connection.getInstance().getSelf();
@@ -74,7 +81,19 @@ public class MainController implements Initializable {
throw new RuntimeException(e);
}
Connection.getInstance().getPacketHandler().addListener(TopicCreatedPacket.class, packet -> {
setUpPacketHandler(Connection.getInstance().getPacketHandler());
Connection.getInstance().beginHandlingPackets();
vboxTopics.getChildren().clear();
Connection.getInstance().addStatListener(evt -> {
Platform.runLater(this::rebuildDebugStatText);
});
}
private void setUpPacketHandler(PacketHandler packetHandler) {
packetHandler.addListener(TopicCreatedPacket.class, packet -> {
User host = Connection.getInstance().getUser(packet.getHostID());
Topic topic = new Topic(
@@ -85,10 +104,12 @@ public class MainController implements Initializable {
packet.getTopicColor()
);
Connection.getInstance().saveTopic(topic);
addTopic(topic);
});
Connection.getInstance().getPacketHandler().addListener(TopicUpdatePacket.class, packet -> {
packetHandler.addListener(TopicUpdatePacket.class, packet -> {
if (!topicMap.containsKey(packet.getTopicID())) return;
Topic topic = topicMap.get(packet.getTopicID()).second().getTopic();
@@ -111,7 +132,7 @@ public class MainController implements Initializable {
}
});
Connection.getInstance().getPacketHandler().addListener(MessageCreatedPacket.class, packet -> {
packetHandler.addListener(MessageCreatedPacket.class, packet -> {
if (!topicMap.containsKey(packet.getTopicID())) return;
Topic topic = topicMap.get(packet.getTopicID()).second().getTopic();
@@ -123,7 +144,7 @@ public class MainController implements Initializable {
topic.addMessage(userMessage);
});
Connection.getInstance().getPacketHandler().addListener(MessageSystemPacket.class, packet -> {
packetHandler.addListener(MessageSystemPacket.class, packet -> {
if (!topicMap.containsKey(packet.getTopicID())) return;
Topic topic = topicMap.get(packet.getTopicID()).second().getTopic();
@@ -138,9 +159,22 @@ public class MainController implements Initializable {
topic.addMessage(systemMessage);
});
Connection.getInstance().beginHandlingPackets();
packetHandler.addListener(TypingListUpdatePacket.class, packet -> {
if (!topicMap.containsKey(packet.getTopicID())) return;
vboxTopics.getChildren().clear();
Topic topic = topicMap.get(packet.getTopicID()).second().getTopic();
Set<Long> unexplored = topic.getTypingUsersReadOnly().stream().map(User::getUserID).collect(Collectors.toSet());
for (LongData typingMember : packet.getTypingMembers()) {
topic.addTypingUser(Connection.getInstance().getUser(typingMember.getValue()));
unexplored.remove(typingMember.getValue());
}
for (Long userID : unexplored) {
topic.removeTypingUser(Connection.getInstance().getUser(userID));
}
});
}
private void fetchTopicMessage(Topic topic, long messageID) {
@@ -265,6 +299,45 @@ public class MainController implements Initializable {
Connection.getInstance().sendPacket(packet);
}
@FXML
void onToggleDebug(MouseEvent event) {
recordDebugStats = !recordDebugStats;
lblDebug.setVisible(recordDebugStats);
Connection.getInstance().setRecordStats(recordDebugStats);
Connection.getInstance().updateConnectionStats();
Connection.getInstance().updateCacheStats();
}
@FXML
void onDebugEnter(MouseEvent event) {
lblDebug.setOpacity(0.9);
}
@FXML
void onDebugLeave(MouseEvent event) {
lblDebug.setOpacity(0.2);
}
private void rebuildDebugStatText() {
StringBuilder sb = new StringBuilder();
sb.append(Connection.getInstance().getCompiledConnectionStats()).append("\n");
sb.append(Connection.getInstance().getCompiledCacheStats()).append("\n");
sb.append("STATS\n");
sb.append("Total Messages (Downloaded): ").append(topicMap.values().stream()
.map(Pair::second)
.map(MainChatController::getTopic)
.map(Topic::getMessagesReadOnly)
.map(List::size)
.reduce(Integer::sum)
.orElse(0));
lblDebug.setText(sb.toString());
}
public static Pair<Parent, MainController> createInstance() {
return Utils.createInstance("views/main-view.fxml");
}
@@ -11,16 +11,16 @@ import java.util.Arrays;
public class UserAvatar {
public static final UserAvatar MORNING = new UserAvatar("Morning", "Eggs and Bacon", 0, "avatars/avatar-0.png");
public static final UserAvatar VINTAGE = new UserAvatar("Vintage", "Red Car", 1, "avatars/avatar-1.png");
public static final UserAvatar ROUTINE = new UserAvatar("Routine", "Coffee Cup", 2, "avatars/avatar-2.png");
public static final UserAvatar SILLY = new UserAvatar("Silly", "Ghost Toy", 3, "avatars/avatar-3.png");
public static final UserAvatar ADVENTURE = new UserAvatar("Adventure", "Slide with a Plant", 4, "avatars/avatar-4.png");
public static final UserAvatar REBELLION = new UserAvatar("Rebellion", "Potted Cactus", 5, "avatars/avatar-5.png");
public static final UserAvatar DIGITAL = new UserAvatar("Digital", "Game Controller", 6, "avatars/avatar-6.png");
public static final UserAvatar MYSTERY = new UserAvatar("Mystery", "Floating Sphere", 7, "avatars/avatar-7.png");
public static final UserAvatar VACATION = new UserAvatar("Vacation", "Cat under an Umbrella", 8, "avatars/avatar-8.png");
public static final UserAvatar SIMPLE = new UserAvatar("Simple", "Potted Succulent", 9, "avatars/avatar-9.png");
public static final UserAvatar MORNING = new UserAvatar("Morning", "Eggs and Bacon", 0, "#facb01", "avatars/avatar-0.png");
public static final UserAvatar VINTAGE = new UserAvatar("Vintage", "Red Car", 1, "#dd2046", "avatars/avatar-1.png");
public static final UserAvatar ROUTINE = new UserAvatar("Routine", "Coffee Cup", 2, "#01b4da", "avatars/avatar-2.png");
public static final UserAvatar SILLY = new UserAvatar("Silly", "Ghost Toy", 3, "#8f58d8", "avatars/avatar-3.png");
public static final UserAvatar ADVENTURE = new UserAvatar("Adventure", "Slide with a Plant", 4, "#6bc7da", "avatars/avatar-4.png");
public static final UserAvatar REBELLION = new UserAvatar("Rebellion", "Potted Cactus", 5, "#17b479", "avatars/avatar-5.png");
public static final UserAvatar DIGITAL = new UserAvatar("Digital", "Game Controller", 6, "#c3c1cf", "avatars/avatar-6.png");
public static final UserAvatar MYSTERY = new UserAvatar("Mystery", "Floating Sphere", 7, "#26badc", "avatars/avatar-7.png");
public static final UserAvatar VACATION = new UserAvatar("Vacation", "Cat under an Umbrella", 8, "#f994fb", "avatars/avatar-8.png");
public static final UserAvatar SIMPLE = new UserAvatar("Simple", "Potted Succulent", 9, "#ff7e29", "avatars/avatar-9.png");
public static UserAvatar getAvatar(int avatarID) {
return Arrays.stream(getAvatars())
@@ -47,12 +47,14 @@ public class UserAvatar {
private final String name;
private final String description;
private final int id;
private final String color;
private final URL url;
private UserAvatar(String name, String description, int id, String path) {
private UserAvatar(String name, String description, int id, String color, String path) {
this.name = name;
this.description = description;
this.id = id;
this.color = color;
this.url = Program.class.getResource(path);
}
@@ -68,6 +70,10 @@ public class UserAvatar {
return id;
}
public String getColor() {
return color;
}
public URL getUrl() {
return url;
}
@@ -1,27 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.String?>
<?import java.net.URL?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.FlowPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.VBox?>
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" styleClass="gradient" xmlns="http://javafx.com/javafx/20.0.1" xmlns:fx="http://javafx.com/fxml/1">
<VBox fx:id="vboxRoot" alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="621.0" spacing="16.0" styleClass="gradient" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="dev.wiing.gossip.client.controllers.AvatarController">
<children>
<Label style="-fx-font-size: 20;" styleClass="axis" text="Choose an Avatar" />
<FlowPane prefHeight="200.0" prefWidth="200.0">
<VBox alignment="CENTER" spacing="16.0" VBox.vgrow="ALWAYS">
<children>
<VBox prefHeight="200.0" prefWidth="100.0">
<Label style="-fx-font-size: 20;" styleClass="axis" text="Choose an Avatar" />
<Button fx:id="btnSave" mnemonicParsing="false" styleClass="axis" text="Save" />
<ScrollPane fitToWidth="true" hbarPolicy="NEVER" vbarPolicy="ALWAYS" VBox.vgrow="ALWAYS">
<styleClass>
<String fx:value="container" />
<String fx:value="list" />
<String fx:value="transparent" />
</styleClass>
<VBox.margin>
<Insets left="11.0" />
</VBox.margin>
<padding>
<Insets left="8.0" right="8.0" />
</padding>
<content>
<FlowPane fx:id="flowAvatars" alignment="CENTER" columnHalignment="CENTER" hgap="16.0" prefWrapLength="0.0" vgap="16.0">
<children>
<VBox fx:id="vboxAvatarButton" alignment="CENTER" spacing="16.0">
<children>
<Pane maxHeight="64.0" maxWidth="64.0" minHeight="64.0" minWidth="64.0" />
<Label text="AvatarName">
<styleClass>
<String fx:value="axis" />
<String fx:value="accent" />
</styleClass>
</Label>
</children>
</VBox>
</children>
</FlowPane>
</content>
</ScrollPane>
</children>
</VBox>
<HBox alignment="CENTER" spacing="16.0">
<children>
<Pane fx:id="paneSelectedIcon" maxHeight="96.0" maxWidth="96.0" minHeight="96.0" minWidth="96.0" />
<VBox alignment="CENTER_LEFT" HBox.hgrow="ALWAYS">
<children>
<Pane maxHeight="64.0" maxWidth="64.0" minHeight="64.0" minWidth="64.0" />
<Label text="Selected">
<styleClass>
<String fx:value="axis" />
<String fx:value="faint" />
</styleClass>
</Label>
<Label fx:id="lblSelectedName" style="-fx-font-size: 16;" text="AvatarName">
<styleClass>
<String fx:value="axis" />
<String fx:value="accent" />
</styleClass>
</Label>
<Label fx:id="lblSelectedDesc" style="-fx-font-size: 14;" text="Description" />
</children>
</VBox>
</children>
</FlowPane>
</HBox>
</children>
<stylesheets>
<URL value="@../styling.css" />
<URL value="@../custom.css" />
<URL value="@../scroll.css" />
</stylesheets>
<padding>
<Insets bottom="16.0" left="16.0" right="16.0" top="16.0" />
</padding>
</VBox>
@@ -15,7 +15,7 @@
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<AnchorPane prefHeight="318.0" prefWidth="523.0" xmlns="http://javafx.com/javafx/20.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="dev.wiing.gossip.client.controllers.MainChatController">
<AnchorPane prefHeight="318.0" prefWidth="523.0" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="dev.wiing.gossip.client.controllers.MainChatController">
<children>
<VBox fx:id="vboxVisitPage" alignment="CENTER" layoutX="10.0" layoutY="10.0" spacing="8.0" visible="false" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
@@ -88,7 +88,7 @@
<String fx:value="list" />
</styleClass>
<children>
<TextArea fx:id="txtCompose" maxHeight="1.7976931348623157E308" minHeight="1.0" prefHeight="25.0" prefRowCount="1" promptText="Compose..." styleClass="transparent" wrapText="true" HBox.hgrow="ALWAYS" />
<TextArea fx:id="txtCompose" maxHeight="1.7976931348623157E308" minHeight="1.0" onKeyPressed="#onKeyPressed" onKeyTyped="#onKeyTyped" prefHeight="25.0" prefRowCount="1" promptText="Compose..." styleClass="transparent" wrapText="true" HBox.hgrow="ALWAYS" />
<ImageView fitHeight="20.0" fitWidth="20.0" onMouseClicked="#onSend" opacity="0.5" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../icons/icon-send-2.png" />
@@ -12,7 +12,7 @@
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" spacing="16.0" styleClass="gradient" xmlns="http://javafx.com/javafx/20.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="dev.wiing.gossip.client.controllers.LoginController">
<VBox fx:id="vboxRoot" alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" spacing="16.0" styleClass="gradient" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="dev.wiing.gossip.client.controllers.LoginController">
<stylesheets>
<URL value="@../styling.css" />
<URL value="@../scroll.css" />
@@ -32,7 +32,7 @@
</Label>
<HBox alignment="CENTER" spacing="16.0" VBox.vgrow="ALWAYS">
<children>
<AnchorPane fx:id="paneIcon" maxHeight="64.0" maxWidth="64.0" minHeight="64.0" minWidth="64.0" onMouseClicked="#onChangeIcon" onMouseEntered="#onIconMouseEnter" onMouseExited="#onIconMouseExit" style="-fx-background-color: white;">
<AnchorPane fx:id="paneIcon" maxHeight="64.0" maxWidth="64.0" minHeight="64.0" minWidth="64.0" onMouseClicked="#onChangeIcon" onMouseEntered="#onIconMouseEnter" onMouseExited="#onIconMouseExit" style="-fx-background-color: white;" styleClass="clickable">
<children>
<HBox fx:id="hboxEditOverlay" alignment="CENTER" mouseTransparent="true" opacity="0.0" style="-fx-background-color: #0006;" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
@@ -17,264 +17,282 @@
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<VBox fx:id="vboxRoot" alignment="TOP_RIGHT" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="486.0" prefWidth="750.0" spacing="16.0" style="-accent: hsb(0, 0%, 50%);;" styleClass="gradient" xmlns="http://javafx.com/javafx/20.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="dev.wiing.gossip.client.controllers.MainController">
<stylesheets>
<URL value="@../styling.css" />
<URL value="@../scroll.css" />
<URL value="@../custom.css" />
</stylesheets>
<AnchorPane stylesheets="@../styling.css" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="dev.wiing.gossip.client.controllers.MainController">
<children>
<HBox>
<VBox fx:id="vboxRoot" alignment="TOP_RIGHT" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="486.0" prefWidth="750.0" spacing="16.0" style="-accent: hsb(0, 0%, 50%);;" styleClass="gradient" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<stylesheets>
<URL value="@../styling.css" />
<URL value="@../scroll.css" />
<URL value="@../custom.css" />
</stylesheets>
<children>
<ImageView blendMode="OVERLAY" fitHeight="30.0" fitWidth="200.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../logo.png" />
</image>
</ImageView>
<Separator maxWidth="1.7976931348623157E308" visible="false" HBox.hgrow="ALWAYS" />
<HBox spacing="8.0">
<HBox>
<children>
<VBox alignment="CENTER_RIGHT" minHeight="40.0" spacing="4.0" HBox.hgrow="ALWAYS">
<ImageView blendMode="OVERLAY" fitHeight="30.0" fitWidth="200.0" onMouseClicked="#onToggleDebug" onMouseEntered="#onDebugEnter" onMouseExited="#onDebugLeave" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../logo.png" />
</image>
</ImageView>
<Separator maxWidth="1.7976931348623157E308" visible="false" HBox.hgrow="ALWAYS" />
<HBox spacing="8.0">
<children>
<HBox alignment="BOTTOM_LEFT" spacing="8.0">
<VBox alignment="CENTER_RIGHT" minHeight="40.0" spacing="4.0" HBox.hgrow="ALWAYS">
<children>
<Label fx:id="lblUsername" style="-fx-font-size: 14;" text="\@ Username">
<HBox alignment="BOTTOM_LEFT" spacing="8.0">
<children>
<Label fx:id="lblUsername" style="-fx-font-size: 14;" text="\@ Username">
<styleClass>
<String fx:value="axis" />
<String fx:value="accent" />
</styleClass>
</Label>
</children>
</HBox>
<Label style="-fx-font-size: 12;" text="Log out">
<styleClass>
<String fx:value="axis" />
<String fx:value="accent" />
<String fx:value="faint" />
</styleClass>
</Label>
</children>
</HBox>
<Label style="-fx-font-size: 12;" text="Log out">
<styleClass>
<String fx:value="axis" />
<String fx:value="faint" />
</styleClass>
</Label>
</VBox>
<Pane fx:id="paneIcon" maxHeight="40.0" maxWidth="40.0" minHeight="40.0" minWidth="40.0" style="-fx-background-color: white;" />
</children>
</VBox>
<Pane fx:id="paneIcon" maxHeight="40.0" maxWidth="40.0" minHeight="40.0" minWidth="40.0" style="-fx-background-color: white;" />
</HBox>
</children>
</HBox>
</children>
</HBox>
<SplitPane dividerPositions="0.29797979797979796" prefHeight="160.0" prefWidth="200.0" styleClass="transparent" VBox.vgrow="ALWAYS">
<items>
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0">
<styleClass>
<String fx:value="container" />
<String fx:value="pane" />
</styleClass>
<children>
<VBox alignment="TOP_CENTER" spacing="16.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<ScrollPane fitToHeight="true" fitToWidth="true" hbarPolicy="NEVER" VBox.vgrow="ALWAYS">
<content>
<VBox fx:id="vboxTopics" maxHeight="1.7976931348623157E308" spacing="8.0">
<children>
<HBox spacing="4.0">
<styleClass>
<String fx:value="tag-min" />
<String fx:value="border-radius-small" />
</styleClass>
<children>
<ImageView blendMode="OVERLAY" fitHeight="16.0" fitWidth="16.0" opacity="0.4" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../icons/icon-hash.png" />
</image>
</ImageView>
<Label text="Label" />
</children>
<padding>
<Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
</padding>
</HBox>
<HBox layoutX="10.0" layoutY="10.0" opacity="0.8" spacing="4.0" styleClass="border-radius-small">
<children>
<ImageView blendMode="OVERLAY" fitHeight="16.0" fitWidth="16.0" opacity="0.4" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../icons/icon-hash.png" />
</image>
</ImageView>
<Label text="Label" />
</children>
<padding>
<Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
</padding>
</HBox>
<HBox layoutX="10.0" layoutY="50.0" opacity="0.8" spacing="4.0" styleClass="border-radius-small">
<children>
<ImageView blendMode="OVERLAY" fitHeight="16.0" fitWidth="16.0" opacity="0.4" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../icons/icon-hash.png" />
</image>
</ImageView>
<Label text="Label" />
</children>
<padding>
<Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
</padding>
</HBox>
</children>
</VBox>
</content>
<padding>
<Insets right="4.0" />
</padding>
<styleClass>
<String fx:value="container" />
<String fx:value="list" />
<String fx:value="transparent" />
</styleClass>
</ScrollPane>
<Button mnemonicParsing="false" onAction="#onCreateTopic" text="New TOPIC">
<styleClass>
<String fx:value="axis" />
<String fx:value="tag" />
</styleClass>
</Button>
</children>
</VBox>
</children>
<padding>
<Insets right="16.0" />
</padding>
</AnchorPane>
<AnchorPane fx:id="paneContent" minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0">
<children>
<Label fx:id="lblJoinMessage" alignment="CENTER" style="-fx-font-size: 20;" text="Start or Join a Topic!" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<SplitPane dividerPositions="0.29797979797979796" prefHeight="160.0" prefWidth="200.0" styleClass="transparent" VBox.vgrow="ALWAYS">
<items>
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0">
<styleClass>
<String fx:value="axis" />
<String fx:value="accent" />
<String fx:value="container" />
<String fx:value="pane" />
</styleClass>
</Label>
<AnchorPane layoutX="16.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<VBox alignment="CENTER" spacing="8.0" visible="false" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<VBox alignment="TOP_CENTER" spacing="16.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<AnchorPane maxHeight="64.0" maxWidth="64.0" minHeight="64.0" minWidth="64.0">
<children>
<Circle blendMode="OVERLAY" fill="WHITE" opacity="0.7" radius="32.0" stroke="BLACK" strokeType="INSIDE" strokeWidth="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="0.0" />
<ImageView fitHeight="48.0" fitWidth="48.0" layoutX="8.0" layoutY="8.0" pickOnBounds="true" preserveRatio="true" AnchorPane.bottomAnchor="8.0" AnchorPane.leftAnchor="8.0" AnchorPane.rightAnchor="8.0" AnchorPane.topAnchor="8.0">
<image>
<Image url="@../icons/icon-hash.png" />
</image>
</ImageView>
</children>
</AnchorPane>
<Label style="-fx-font-size: 20;" styleClass="axis" text="Topic name" />
<Label text="Topic Description" />
<Label styleClass="accent" text="\@ Username" />
<Separator orientation="VERTICAL" prefHeight="20.0" visible="false" />
<Button mnemonicParsing="false" text="Join">
<styleClass>
<String fx:value="axis" />
<String fx:value="secondary" />
</styleClass>
</Button>
<Label styleClass="faint" text="2 babblers active" />
</children>
</VBox>
<VBox prefHeight="200.0" prefWidth="100.0" spacing="16.0" visible="false" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<HBox alignment="CENTER_LEFT" spacing="4.0">
<children>
<ImageView fitHeight="24.0" fitWidth="24.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../icons/icon-hash.png" />
</image>
</ImageView>
<Label style="-fx-font-size: 16;" styleClass="axis" text="Label" />
<Label alignment="CENTER_RIGHT" maxWidth="1.7976931348623157E308" style="-fx-font-size: 12;" styleClass="faint" text="2 babblers" HBox.hgrow="ALWAYS" />
</children>
</HBox>
<ScrollPane fitToHeight="true" fitToWidth="true" VBox.vgrow="ALWAYS">
<ScrollPane fitToHeight="true" fitToWidth="true" hbarPolicy="NEVER" VBox.vgrow="ALWAYS">
<content>
<VBox spacing="16.0">
<VBox fx:id="vboxTopics" maxHeight="1.7976931348623157E308" spacing="8.0">
<children>
<HBox spacing="8.0">
<HBox spacing="4.0">
<styleClass>
<String fx:value="tag-min" />
<String fx:value="border-radius-small" />
</styleClass>
<children>
<Pane maxHeight="40.0" maxWidth="40.0" minHeight="40.0" minWidth="40.0" style="-fx-background-color: white;" />
<VBox alignment="CENTER_LEFT" minHeight="40.0" spacing="4.0" HBox.hgrow="ALWAYS">
<children>
<HBox alignment="BOTTOM_LEFT" spacing="8.0">
<children>
<Label style="-fx-font-size: 14;" text="\@ Username">
<styleClass>
<String fx:value="axis" />
<String fx:value="accent" />
</styleClass>
</Label>
<Label text="You">
<styleClass>
<String fx:value="tag" />
<String fx:value="small" />
<String fx:value="axis" />
</styleClass>
</Label>
<Label alignment="CENTER_RIGHT" contentDisplay="RIGHT" maxWidth="1.7976931348623157E308" style="-fx-font-size: 10;" styleClass="faint" text="10 minutes ago" textAlignment="RIGHT" HBox.hgrow="ALWAYS" />
</children>
</HBox>
<VBox spacing="4.0">
<children>
<Label text="Message contents" />
</children>
</VBox>
</children>
</VBox>
<ImageView blendMode="OVERLAY" fitHeight="16.0" fitWidth="16.0" opacity="0.4" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../icons/icon-hash.png" />
</image>
</ImageView>
<Label text="Label" />
</children>
<padding>
<Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
</padding>
</HBox>
<HBox layoutX="10.0" layoutY="10.0" opacity="0.8" spacing="4.0" styleClass="border-radius-small">
<children>
<ImageView blendMode="OVERLAY" fitHeight="16.0" fitWidth="16.0" opacity="0.4" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../icons/icon-hash.png" />
</image>
</ImageView>
<Label text="Label" />
</children>
<padding>
<Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
</padding>
</HBox>
<HBox layoutX="10.0" layoutY="50.0" opacity="0.8" spacing="4.0" styleClass="border-radius-small">
<children>
<ImageView blendMode="OVERLAY" fitHeight="16.0" fitWidth="16.0" opacity="0.4" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../icons/icon-hash.png" />
</image>
</ImageView>
<Label text="Label" />
</children>
<padding>
<Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
</padding>
</HBox>
</children>
<padding>
<Insets bottom="16.0" left="16.0" right="16.0" top="16.0" />
</padding>
</VBox>
</content>
<padding>
<Insets right="4.0" />
</padding>
<styleClass>
<String fx:value="container" />
<String fx:value="list" />
<String fx:value="transparent" />
</styleClass>
</ScrollPane>
<VBox spacing="8.0">
<children>
<Label style="-fx-font-size: 11;" styleClass="faint" text="Username, Username is typing ...">
<padding>
<Insets left="16.0" right="16.0" />
</padding>
</Label>
<HBox alignment="CENTER_LEFT" spacing="8.0">
<styleClass>
<String fx:value="container" />
<String fx:value="pane" />
<String fx:value="list" />
</styleClass>
<children>
<TextArea maxHeight="1.7976931348623157E308" minHeight="1.0" prefHeight="24.0" prefRowCount="1" promptText="Compose..." styleClass="transparent" wrapText="true" HBox.hgrow="ALWAYS" />
<ImageView fitHeight="20.0" fitWidth="20.0" opacity="0.5" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../icons/icon-send-2.png" />
</image>
</ImageView>
</children>
<padding>
<Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
</padding>
</HBox>
</children>
</VBox>
<Button mnemonicParsing="false" onAction="#onCreateTopic" text="New TOPIC">
<styleClass>
<String fx:value="axis" />
<String fx:value="tag" />
</styleClass>
</Button>
</children>
</VBox>
</children>
<padding>
<Insets right="16.0" />
</padding>
</AnchorPane>
</children>
<AnchorPane fx:id="paneContent" minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0">
<children>
<Label fx:id="lblJoinMessage" alignment="CENTER" style="-fx-font-size: 20;" text="Start or Join a Topic!" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<styleClass>
<String fx:value="axis" />
<String fx:value="accent" />
</styleClass>
</Label>
<AnchorPane layoutX="16.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<VBox alignment="CENTER" spacing="8.0" visible="false" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<AnchorPane maxHeight="64.0" maxWidth="64.0" minHeight="64.0" minWidth="64.0">
<children>
<Circle blendMode="OVERLAY" fill="WHITE" opacity="0.7" radius="32.0" stroke="BLACK" strokeType="INSIDE" strokeWidth="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="0.0" />
<ImageView fitHeight="48.0" fitWidth="48.0" layoutX="8.0" layoutY="8.0" pickOnBounds="true" preserveRatio="true" AnchorPane.bottomAnchor="8.0" AnchorPane.leftAnchor="8.0" AnchorPane.rightAnchor="8.0" AnchorPane.topAnchor="8.0">
<image>
<Image url="@../icons/icon-hash.png" />
</image>
</ImageView>
</children>
</AnchorPane>
<Label style="-fx-font-size: 20;" styleClass="axis" text="Topic name" />
<Label text="Topic Description" />
<Label styleClass="accent" text="\@ Username" />
<Separator orientation="VERTICAL" prefHeight="20.0" visible="false" />
<Button mnemonicParsing="false" text="Join">
<styleClass>
<String fx:value="axis" />
<String fx:value="secondary" />
</styleClass>
</Button>
<Label styleClass="faint" text="2 babblers active" />
</children>
</VBox>
<VBox prefHeight="200.0" prefWidth="100.0" spacing="16.0" visible="false" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<HBox alignment="CENTER_LEFT" spacing="4.0">
<children>
<ImageView fitHeight="24.0" fitWidth="24.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../icons/icon-hash.png" />
</image>
</ImageView>
<Label style="-fx-font-size: 16;" styleClass="axis" text="Label" />
<Label alignment="CENTER_RIGHT" maxWidth="1.7976931348623157E308" style="-fx-font-size: 12;" styleClass="faint" text="2 babblers" HBox.hgrow="ALWAYS" />
</children>
</HBox>
<ScrollPane fitToHeight="true" fitToWidth="true" VBox.vgrow="ALWAYS">
<content>
<VBox spacing="16.0">
<children>
<HBox spacing="8.0">
<children>
<Pane maxHeight="40.0" maxWidth="40.0" minHeight="40.0" minWidth="40.0" style="-fx-background-color: white;" />
<VBox alignment="CENTER_LEFT" minHeight="40.0" spacing="4.0" HBox.hgrow="ALWAYS">
<children>
<HBox alignment="BOTTOM_LEFT" spacing="8.0">
<children>
<Label style="-fx-font-size: 14;" text="\@ Username">
<styleClass>
<String fx:value="axis" />
<String fx:value="accent" />
</styleClass>
</Label>
<Label text="You">
<styleClass>
<String fx:value="tag" />
<String fx:value="small" />
<String fx:value="axis" />
</styleClass>
</Label>
<Label alignment="CENTER_RIGHT" contentDisplay="RIGHT" maxWidth="1.7976931348623157E308" style="-fx-font-size: 10;" styleClass="faint" text="10 minutes ago" textAlignment="RIGHT" HBox.hgrow="ALWAYS" />
</children>
</HBox>
<VBox spacing="4.0">
<children>
<Label text="Message contents" />
</children>
</VBox>
</children>
</VBox>
</children>
</HBox>
</children>
<padding>
<Insets bottom="16.0" left="16.0" right="16.0" top="16.0" />
</padding>
</VBox>
</content>
<styleClass>
<String fx:value="container" />
<String fx:value="list" />
</styleClass>
</ScrollPane>
<VBox spacing="8.0">
<children>
<Label style="-fx-font-size: 11;" styleClass="faint" text="Username, Username is typing ...">
<padding>
<Insets left="16.0" right="16.0" />
</padding>
</Label>
<HBox alignment="CENTER_LEFT" spacing="8.0">
<styleClass>
<String fx:value="container" />
<String fx:value="pane" />
<String fx:value="list" />
</styleClass>
<children>
<TextArea maxHeight="1.7976931348623157E308" minHeight="1.0" prefHeight="24.0" prefRowCount="1" promptText="Compose..." styleClass="transparent" wrapText="true" HBox.hgrow="ALWAYS" />
<ImageView fitHeight="20.0" fitWidth="20.0" opacity="0.5" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../icons/icon-send-2.png" />
</image>
</ImageView>
</children>
<padding>
<Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
</padding>
</HBox>
</children>
</VBox>
</children>
</VBox>
</children>
</AnchorPane>
</children>
<padding>
<Insets left="16.0" />
</padding>
</AnchorPane>
</items>
</SplitPane>
</children>
<padding>
<Insets bottom="16.0" left="16.0" right="16.0" top="16.0" />
</padding>
</VBox>
<VBox alignment="BOTTOM_RIGHT" mouseTransparent="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<Label fx:id="lblDebug" alignment="BOTTOM_RIGHT" opacity="0.2" style="-fx-background-color: #000f;" styleClass="border-radius-small" text="Debug" visible="false">
<padding>
<Insets left="16.0" />
</padding></AnchorPane>
</items>
</SplitPane>
<Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
</padding>
</Label>
</children>
<padding>
<Insets bottom="16.0" left="16.0" right="16.0" top="16.0" />
</padding>
</VBox>
</children>
<padding>
<Insets bottom="16.0" left="16.0" right="16.0" top="16.0" />
</padding>
</VBox>
</AnchorPane>