Typing indicator + Metrics
This commit is contained in:
parent
ac58009119
commit
a477a87c3d
@ -2,12 +2,15 @@ package dev.wiing.gossip.client;
|
|||||||
|
|
||||||
import dev.wiing.gossip.lib.PacketHandler;
|
import dev.wiing.gossip.lib.PacketHandler;
|
||||||
import dev.wiing.gossip.lib.PacketManager;
|
import dev.wiing.gossip.lib.PacketManager;
|
||||||
|
import dev.wiing.gossip.lib.PacketMetrics;
|
||||||
import dev.wiing.gossip.lib.data.LongData;
|
import dev.wiing.gossip.lib.data.LongData;
|
||||||
import dev.wiing.gossip.lib.models.SecretUser;
|
import dev.wiing.gossip.lib.models.SecretUser;
|
||||||
import dev.wiing.gossip.lib.models.Topic;
|
import dev.wiing.gossip.lib.models.Topic;
|
||||||
import dev.wiing.gossip.lib.models.User;
|
import dev.wiing.gossip.lib.models.User;
|
||||||
import dev.wiing.gossip.lib.packets.*;
|
import dev.wiing.gossip.lib.packets.*;
|
||||||
|
|
||||||
|
import java.beans.PropertyChangeListener;
|
||||||
|
import java.beans.PropertyChangeSupport;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
@ -18,6 +21,13 @@ public class Connection {
|
|||||||
|
|
||||||
private static final Connection instance = new 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 PacketManager packetManager = new PacketManager();
|
||||||
|
|
||||||
private final PacketHandler packetHandler = new PacketHandler();
|
private final PacketHandler packetHandler = new PacketHandler();
|
||||||
@ -32,6 +42,7 @@ public class Connection {
|
|||||||
|
|
||||||
private Connection() {
|
private Connection() {
|
||||||
packetManager.registerPackets();
|
packetManager.registerPackets();
|
||||||
|
changeSupport = new PropertyChangeSupport(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Connection getInstance() {
|
public static Connection getInstance() {
|
||||||
@ -51,6 +62,8 @@ public class Connection {
|
|||||||
|
|
||||||
public PacketHandlerRunnable(Connection connection) {
|
public PacketHandlerRunnable(Connection connection) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
|
|
||||||
|
connection.packetHandler.addAllPacketsToMap(connection.packetManager.getPacketMap().values());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -69,6 +82,8 @@ public class Connection {
|
|||||||
if (!connection.getPacketHandler().runPacket(packet)) {
|
if (!connection.getPacketHandler().runPacket(packet)) {
|
||||||
connection.queuedPackets.add(packet);
|
connection.queuedPackets.add(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connection.updateConnectionStats();
|
||||||
} catch (SocketException e) {
|
} catch (SocketException e) {
|
||||||
break;
|
break;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -83,6 +98,91 @@ public class Connection {
|
|||||||
handlerThread.start();
|
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() {
|
public Socket getSocket() {
|
||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
@ -134,6 +234,8 @@ public class Connection {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateConnectionStats();
|
||||||
|
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -192,6 +294,10 @@ public class Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getQueueSize() {
|
||||||
|
return queuedPackets.size();
|
||||||
|
}
|
||||||
|
|
||||||
public SecretUser getSelf() {
|
public SecretUser getSelf() {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@ -230,7 +336,20 @@ public class Connection {
|
|||||||
});
|
});
|
||||||
|
|
||||||
public User getUser(long userID) {
|
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 -> {
|
private final DataCache<Topic> topicCache = new DataCache<>(id -> {
|
||||||
@ -263,6 +382,19 @@ public class Connection {
|
|||||||
});
|
});
|
||||||
|
|
||||||
public Topic getTopic(long topicID) {
|
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,6 +30,7 @@ public class DataCache<T> {
|
|||||||
|
|
||||||
T result;
|
T result;
|
||||||
|
|
||||||
|
synchronized (cache) {
|
||||||
if ((result = cache.getOrDefault(id, null)) != null) {
|
if ((result = cache.getOrDefault(id, null)) != null) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -38,6 +39,7 @@ public class DataCache<T> {
|
|||||||
if (result != null) {
|
if (result != null) {
|
||||||
cache.put(id, result);
|
cache.put(id, result);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -45,4 +47,8 @@ public class DataCache<T> {
|
|||||||
public void put(long id, T val) {
|
public void put(long id, T val) {
|
||||||
cache.put(id, 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.Connection;
|
||||||
import dev.wiing.gossip.client.data.UserAvatar;
|
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.models.SecretUser;
|
||||||
import dev.wiing.gossip.lib.packets.RegisterCredentialsPacket;
|
import dev.wiing.gossip.lib.packets.RegisterCredentialsPacket;
|
||||||
import dev.wiing.gossip.lib.packets.Packet;
|
import dev.wiing.gossip.lib.packets.Packet;
|
||||||
@ -35,6 +36,11 @@ public class LoginController implements Initializable {
|
|||||||
@FXML
|
@FXML
|
||||||
private TextField txtUsername;
|
private TextField txtUsername;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private VBox vboxRoot;
|
||||||
|
|
||||||
|
private UserAvatar selectedAvatar = UserAvatar.MORNING;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
public void initialize(URL location, ResourceBundle resources) {
|
||||||
UserAvatar.MORNING.applyToRegionBackground(paneIcon, false);
|
UserAvatar.MORNING.applyToRegionBackground(paneIcon, false);
|
||||||
@ -42,17 +48,31 @@ public class LoginController implements Initializable {
|
|||||||
Circle circle = new Circle(paneIcon.getMinWidth() * 0.5);
|
Circle circle = new Circle(paneIcon.getMinWidth() * 0.5);
|
||||||
paneIcon.setShape(circle);
|
paneIcon.setShape(circle);
|
||||||
hboxEditOverlay.setShape(circle);
|
hboxEditOverlay.setShape(circle);
|
||||||
|
|
||||||
|
vboxRoot.setStyle("-accent: " + selectedAvatar.getColor());
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void onChangeIcon(MouseEvent event) {
|
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
|
@FXML
|
||||||
public void onLogin(ActionEvent event) {
|
public void onLogin(ActionEvent event) {
|
||||||
RegisterRequestPacket packet = new RegisterRequestPacket();
|
RegisterRequestPacket packet = new RegisterRequestPacket();
|
||||||
packet.setAvatarID((byte)0);
|
packet.setAvatarID((byte)selectedAvatar.getId());
|
||||||
packet.setUsername(txtUsername.getText());
|
packet.setUsername(txtUsername.getText());
|
||||||
|
|
||||||
Connection.getInstance().sendPacket(packet);
|
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.generic.Pair;
|
||||||
import dev.wiing.gossip.client.utils.Utils;
|
import dev.wiing.gossip.client.utils.Utils;
|
||||||
import dev.wiing.gossip.lib.models.SystemMessage;
|
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.UserMessage;
|
||||||
import dev.wiing.gossip.lib.models.Topic;
|
import dev.wiing.gossip.lib.models.Topic;
|
||||||
import dev.wiing.gossip.lib.packets.MessagePushPacket;
|
import dev.wiing.gossip.lib.packets.MessagePushPacket;
|
||||||
import dev.wiing.gossip.lib.packets.TopicJoinPacket;
|
import dev.wiing.gossip.lib.packets.TopicJoinPacket;
|
||||||
|
import dev.wiing.gossip.lib.packets.TypingPingPacket;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.Parent;
|
import javafx.scene.Parent;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.TextArea;
|
import javafx.scene.control.TextArea;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyEvent;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
import java.beans.PropertyChangeEvent;
|
import java.beans.PropertyChangeEvent;
|
||||||
@ -64,6 +69,10 @@ public class MainChatController {
|
|||||||
@FXML
|
@FXML
|
||||||
private VBox vboxVisitPage;
|
private VBox vboxVisitPage;
|
||||||
|
|
||||||
|
private Thread typingWaitThread = null;
|
||||||
|
|
||||||
|
private boolean composeFieldPreviouslyEmpty = true;
|
||||||
|
|
||||||
public void setTopic(Topic topic) {
|
public void setTopic(Topic topic) {
|
||||||
this.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(() -> {
|
Platform.runLater(() -> {
|
||||||
if (evt.getNewValue() instanceof UserMessage) {
|
if (evt.getNewValue() instanceof UserMessage) {
|
||||||
addMessage((UserMessage)evt.getNewValue());
|
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) {
|
private void setTopicName(String name) {
|
||||||
@ -163,24 +178,111 @@ public class MainChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
private void sendMessage() {
|
||||||
void onSend(MouseEvent event) {
|
|
||||||
String messageContents = txtCompose.getText();
|
String messageContents = txtCompose.getText();
|
||||||
|
|
||||||
|
if (messageContents.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
composeFieldPreviouslyEmpty = true;
|
||||||
txtCompose.clear();
|
txtCompose.clear();
|
||||||
|
|
||||||
MessagePushPacket packet = new MessagePushPacket();
|
MessagePushPacket packet = new MessagePushPacket();
|
||||||
packet.setTopicID(topic.getId());
|
packet.setTopicID(topic.getId());
|
||||||
packet.setMessage(messageContents);
|
packet.setMessage(messageContents.strip());
|
||||||
|
|
||||||
Connection.getInstance().sendPacketAuthenticated(packet);
|
Connection.getInstance().sendPacketAuthenticated(packet);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Connection.getInstance().findAck(MessagePushPacket.TYPE);
|
Connection.getInstance().findAck(MessagePushPacket.TYPE);
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(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() {
|
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.data.UserAvatar;
|
||||||
import dev.wiing.gossip.client.generic.Pair;
|
import dev.wiing.gossip.client.generic.Pair;
|
||||||
import dev.wiing.gossip.client.utils.Utils;
|
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.data.LongData;
|
||||||
import dev.wiing.gossip.lib.models.*;
|
import dev.wiing.gossip.lib.models.*;
|
||||||
import dev.wiing.gossip.lib.packets.*;
|
import dev.wiing.gossip.lib.packets.*;
|
||||||
@ -14,15 +15,16 @@ import javafx.fxml.FXML;
|
|||||||
import javafx.fxml.Initializable;
|
import javafx.fxml.Initializable;
|
||||||
import javafx.scene.Parent;
|
import javafx.scene.Parent;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.scene.layout.AnchorPane;
|
import javafx.scene.layout.AnchorPane;
|
||||||
import javafx.scene.layout.Pane;
|
import javafx.scene.layout.Pane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Map;
|
import java.util.*;
|
||||||
import java.util.ResourceBundle;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class MainController implements Initializable {
|
public class MainController implements Initializable {
|
||||||
|
|
||||||
@ -48,6 +50,11 @@ public class MainController implements Initializable {
|
|||||||
@FXML
|
@FXML
|
||||||
private Label lblJoinMessage;
|
private Label lblJoinMessage;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label lblDebug;
|
||||||
|
|
||||||
|
private boolean recordDebugStats = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL url, ResourceBundle resourceBundle) {
|
public void initialize(URL url, ResourceBundle resourceBundle) {
|
||||||
User user = Connection.getInstance().getSelf();
|
User user = Connection.getInstance().getSelf();
|
||||||
@ -74,7 +81,19 @@ public class MainController implements Initializable {
|
|||||||
throw new RuntimeException(e);
|
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());
|
User host = Connection.getInstance().getUser(packet.getHostID());
|
||||||
|
|
||||||
Topic topic = new Topic(
|
Topic topic = new Topic(
|
||||||
@ -85,10 +104,12 @@ public class MainController implements Initializable {
|
|||||||
packet.getTopicColor()
|
packet.getTopicColor()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Connection.getInstance().saveTopic(topic);
|
||||||
|
|
||||||
addTopic(topic);
|
addTopic(topic);
|
||||||
});
|
});
|
||||||
|
|
||||||
Connection.getInstance().getPacketHandler().addListener(TopicUpdatePacket.class, packet -> {
|
packetHandler.addListener(TopicUpdatePacket.class, packet -> {
|
||||||
if (!topicMap.containsKey(packet.getTopicID())) return;
|
if (!topicMap.containsKey(packet.getTopicID())) return;
|
||||||
|
|
||||||
Topic topic = topicMap.get(packet.getTopicID()).second().getTopic();
|
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;
|
if (!topicMap.containsKey(packet.getTopicID())) return;
|
||||||
|
|
||||||
Topic topic = topicMap.get(packet.getTopicID()).second().getTopic();
|
Topic topic = topicMap.get(packet.getTopicID()).second().getTopic();
|
||||||
@ -123,7 +144,7 @@ public class MainController implements Initializable {
|
|||||||
topic.addMessage(userMessage);
|
topic.addMessage(userMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
Connection.getInstance().getPacketHandler().addListener(MessageSystemPacket.class, packet -> {
|
packetHandler.addListener(MessageSystemPacket.class, packet -> {
|
||||||
if (!topicMap.containsKey(packet.getTopicID())) return;
|
if (!topicMap.containsKey(packet.getTopicID())) return;
|
||||||
|
|
||||||
Topic topic = topicMap.get(packet.getTopicID()).second().getTopic();
|
Topic topic = topicMap.get(packet.getTopicID()).second().getTopic();
|
||||||
@ -138,9 +159,22 @@ public class MainController implements Initializable {
|
|||||||
topic.addMessage(systemMessage);
|
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) {
|
private void fetchTopicMessage(Topic topic, long messageID) {
|
||||||
@ -265,6 +299,45 @@ public class MainController implements Initializable {
|
|||||||
Connection.getInstance().sendPacket(packet);
|
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() {
|
public static Pair<Parent, MainController> createInstance() {
|
||||||
return Utils.createInstance("views/main-view.fxml");
|
return Utils.createInstance("views/main-view.fxml");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,16 +11,16 @@ import java.util.Arrays;
|
|||||||
|
|
||||||
public class UserAvatar {
|
public class UserAvatar {
|
||||||
|
|
||||||
public static final UserAvatar MORNING = new UserAvatar("Morning", "Eggs and Bacon", 0, "avatars/avatar-0.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, "avatars/avatar-1.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, "avatars/avatar-2.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, "avatars/avatar-3.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, "avatars/avatar-4.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, "avatars/avatar-5.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, "avatars/avatar-6.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, "avatars/avatar-7.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, "avatars/avatar-8.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, "avatars/avatar-9.png");
|
public static final UserAvatar SIMPLE = new UserAvatar("Simple", "Potted Succulent", 9, "#ff7e29", "avatars/avatar-9.png");
|
||||||
|
|
||||||
public static UserAvatar getAvatar(int avatarID) {
|
public static UserAvatar getAvatar(int avatarID) {
|
||||||
return Arrays.stream(getAvatars())
|
return Arrays.stream(getAvatars())
|
||||||
@ -47,12 +47,14 @@ public class UserAvatar {
|
|||||||
private final String name;
|
private final String name;
|
||||||
private final String description;
|
private final String description;
|
||||||
private final int id;
|
private final int id;
|
||||||
|
private final String color;
|
||||||
private final URL url;
|
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.name = name;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
this.color = color;
|
||||||
this.url = Program.class.getResource(path);
|
this.url = Program.class.getResource(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,6 +70,10 @@ public class UserAvatar {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getColor() {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
public URL getUrl() {
|
public URL getUrl() {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,83 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import java.lang.String?>
|
||||||
<?import java.net.URL?>
|
<?import java.net.URL?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
<?import javafx.scene.control.Label?>
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.ScrollPane?>
|
||||||
<?import javafx.scene.layout.FlowPane?>
|
<?import javafx.scene.layout.FlowPane?>
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
<?import javafx.scene.layout.Pane?>
|
<?import javafx.scene.layout.Pane?>
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
|
||||||
|
<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">
|
||||||
<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">
|
<children>
|
||||||
|
<VBox alignment="CENTER" spacing="16.0" VBox.vgrow="ALWAYS">
|
||||||
<children>
|
<children>
|
||||||
<Label style="-fx-font-size: 20;" styleClass="axis" text="Choose an Avatar" />
|
<Label style="-fx-font-size: 20;" styleClass="axis" text="Choose an Avatar" />
|
||||||
<FlowPane prefHeight="200.0" prefWidth="200.0">
|
<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>
|
<children>
|
||||||
<VBox prefHeight="200.0" prefWidth="100.0">
|
<VBox fx:id="vboxAvatarButton" alignment="CENTER" spacing="16.0">
|
||||||
<children>
|
<children>
|
||||||
<Pane maxHeight="64.0" maxWidth="64.0" minHeight="64.0" minWidth="64.0" />
|
<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>
|
</children>
|
||||||
</VBox>
|
</VBox>
|
||||||
</children>
|
</children>
|
||||||
</FlowPane>
|
</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>
|
||||||
|
<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>
|
||||||
|
</HBox>
|
||||||
</children>
|
</children>
|
||||||
<stylesheets>
|
<stylesheets>
|
||||||
<URL value="@../styling.css" />
|
<URL value="@../styling.css" />
|
||||||
<URL value="@../custom.css" />
|
<URL value="@../custom.css" />
|
||||||
|
<URL value="@../scroll.css" />
|
||||||
</stylesheets>
|
</stylesheets>
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="16.0" left="16.0" right="16.0" top="16.0" />
|
||||||
|
</padding>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
<?import javafx.scene.shape.Circle?>
|
<?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>
|
<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">
|
<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>
|
<children>
|
||||||
@ -88,7 +88,7 @@
|
|||||||
<String fx:value="list" />
|
<String fx:value="list" />
|
||||||
</styleClass>
|
</styleClass>
|
||||||
<children>
|
<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">
|
<ImageView fitHeight="20.0" fitWidth="20.0" onMouseClicked="#onSend" opacity="0.5" pickOnBounds="true" preserveRatio="true">
|
||||||
<image>
|
<image>
|
||||||
<Image url="@../icons/icon-send-2.png" />
|
<Image url="@../icons/icon-send-2.png" />
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
<?import javafx.scene.layout.HBox?>
|
<?import javafx.scene.layout.HBox?>
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?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>
|
<stylesheets>
|
||||||
<URL value="@../styling.css" />
|
<URL value="@../styling.css" />
|
||||||
<URL value="@../scroll.css" />
|
<URL value="@../scroll.css" />
|
||||||
@ -32,7 +32,7 @@
|
|||||||
</Label>
|
</Label>
|
||||||
<HBox alignment="CENTER" spacing="16.0" VBox.vgrow="ALWAYS">
|
<HBox alignment="CENTER" spacing="16.0" VBox.vgrow="ALWAYS">
|
||||||
<children>
|
<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>
|
<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">
|
<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>
|
<children>
|
||||||
|
|||||||
@ -17,7 +17,10 @@
|
|||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
<?import javafx.scene.shape.Circle?>
|
<?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">
|
|
||||||
|
<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>
|
||||||
|
<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>
|
<stylesheets>
|
||||||
<URL value="@../styling.css" />
|
<URL value="@../styling.css" />
|
||||||
<URL value="@../scroll.css" />
|
<URL value="@../scroll.css" />
|
||||||
@ -26,7 +29,7 @@
|
|||||||
<children>
|
<children>
|
||||||
<HBox>
|
<HBox>
|
||||||
<children>
|
<children>
|
||||||
<ImageView blendMode="OVERLAY" fitHeight="30.0" fitWidth="200.0" pickOnBounds="true" preserveRatio="true">
|
<ImageView blendMode="OVERLAY" fitHeight="30.0" fitWidth="200.0" onMouseClicked="#onToggleDebug" onMouseEntered="#onDebugEnter" onMouseExited="#onDebugLeave" pickOnBounds="true" preserveRatio="true">
|
||||||
<image>
|
<image>
|
||||||
<Image url="@../logo.png" />
|
<Image url="@../logo.png" />
|
||||||
</image>
|
</image>
|
||||||
@ -270,11 +273,26 @@
|
|||||||
</children>
|
</children>
|
||||||
<padding>
|
<padding>
|
||||||
<Insets left="16.0" />
|
<Insets left="16.0" />
|
||||||
</padding></AnchorPane>
|
</padding>
|
||||||
|
</AnchorPane>
|
||||||
</items>
|
</items>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</children>
|
</children>
|
||||||
<padding>
|
<padding>
|
||||||
<Insets bottom="16.0" left="16.0" right="16.0" top="16.0" />
|
<Insets bottom="16.0" left="16.0" right="16.0" top="16.0" />
|
||||||
</padding>
|
</padding>
|
||||||
</VBox>
|
</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 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>
|
||||||
|
</AnchorPane>
|
||||||
|
|||||||
@ -8,8 +8,23 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
public class PacketHandler {
|
public class PacketHandler {
|
||||||
|
|
||||||
|
private final Map<Class<? extends Packet>, Packet> packetClassMap = new HashMap<>();
|
||||||
|
|
||||||
private final Map<Class<? extends Packet>, Set<PacketListener<? extends Packet>>> listeners = new ConcurrentHashMap<>();
|
private final Map<Class<? extends Packet>, Set<PacketListener<? extends Packet>>> listeners = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public void clearPacketMap() {
|
||||||
|
packetClassMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addPacketToMap(Packet packet) {
|
||||||
|
packetClassMap.put(packet.getClass(), packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAllPacketsToMap(Collection<Packet> packets) {
|
||||||
|
packetClassMap.putAll(packets.stream()
|
||||||
|
.collect(Collectors.toMap(Packet::getClass, o -> o)));
|
||||||
|
}
|
||||||
|
|
||||||
public <T extends Packet> void addListener(Class<T> type, PacketListener<T> listener) {
|
public <T extends Packet> void addListener(Class<T> type, PacketListener<T> listener) {
|
||||||
if (!listeners.containsKey(type)) {
|
if (!listeners.containsKey(type)) {
|
||||||
listeners.put(type, new HashSet<>());
|
listeners.put(type, new HashSet<>());
|
||||||
@ -39,35 +54,25 @@ public class PacketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean runPacket(Packet packet) {
|
public boolean runPacket(Packet packet) {
|
||||||
return switch (packet.getType()) {
|
return runPacket((Class<Packet>)packet.getClass(), packet);
|
||||||
case TopicCreatedPacket.TYPE -> runPacket(TopicCreatedPacket.class, (TopicCreatedPacket) packet);
|
|
||||||
case TopicUpdatePacket.TYPE -> runPacket(TopicUpdatePacket.class, (TopicUpdatePacket) packet);
|
|
||||||
case TopicListDataPacket.TYPE -> runPacket(TopicListDataPacket.class, (TopicListDataPacket) packet);
|
|
||||||
case MessageCreatedPacket.TYPE -> runPacket(MessageCreatedPacket.class, (MessageCreatedPacket) packet);
|
|
||||||
case MessageSystemPacket.TYPE -> runPacket(MessageSystemPacket.class, (MessageSystemPacket) packet);
|
|
||||||
default -> false;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T extends Packet> Set<PacketListener<T>> getListeners(Class<T> packetClass) {
|
public <T extends Packet> Set<PacketListener<T>> getListeners(Class<T> packetClass) {
|
||||||
if (!listeners.containsKey(packetClass))
|
if (!listeners.containsKey(packetClass))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return listeners.get(packetClass).stream().map(listener -> {
|
return listeners.get(packetClass).stream()
|
||||||
return (PacketListener<T>) listener;
|
.map(listener -> (PacketListener<T>) listener)
|
||||||
}).collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Short> getListeningTypes() {
|
public List<Short> getListeningTypes() {
|
||||||
return new ArrayList<>() {
|
return listeners.entrySet().stream()
|
||||||
{
|
.filter(classSetEntry -> !classSetEntry.getValue().isEmpty())
|
||||||
add(TopicCreatedPacket.TYPE);
|
.map(Map.Entry::getKey)
|
||||||
add(TopicUpdatePacket.TYPE);
|
.map(packetClassMap::get)
|
||||||
add(TopicListDataPacket.TYPE);
|
.map(Packet::getType)
|
||||||
add(MessageCreatedPacket.TYPE);
|
.collect(Collectors.toList());
|
||||||
add(MessageSystemPacket.TYPE);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,7 @@ import dev.wiing.gossip.lib.packets.*;
|
|||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class PacketManager {
|
public class PacketManager {
|
||||||
|
|
||||||
@ -38,9 +35,14 @@ public class PacketManager {
|
|||||||
add(new MessageListDataPacket());
|
add(new MessageListDataPacket());
|
||||||
add(new MessageFetchPacket());
|
add(new MessageFetchPacket());
|
||||||
add(new MessageDataPacket());
|
add(new MessageDataPacket());
|
||||||
|
|
||||||
|
add(new TypingPingPacket());
|
||||||
|
add(new TypingListUpdatePacket());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private final PacketMetrics packetMetrics = new PacketMetrics();
|
||||||
|
|
||||||
private final Map<Short, Packet> packetMap = new HashMap<>();
|
private final Map<Short, Packet> packetMap = new HashMap<>();
|
||||||
|
|
||||||
public void addPacket(Packet packet) {
|
public void addPacket(Packet packet) {
|
||||||
@ -58,6 +60,14 @@ public class PacketManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<Short, Packet> getPacketMap() {
|
||||||
|
return Collections.unmodifiableMap(packetMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PacketMetrics getPacketMetrics() {
|
||||||
|
return packetMetrics;
|
||||||
|
}
|
||||||
|
|
||||||
public Packet readPacket(InputStream stream) throws IOException {
|
public Packet readPacket(InputStream stream) throws IOException {
|
||||||
ByteBuffer buffer = ByteBuffer.wrap(stream.readNBytes(6));
|
ByteBuffer buffer = ByteBuffer.wrap(stream.readNBytes(6));
|
||||||
|
|
||||||
@ -68,9 +78,14 @@ public class PacketManager {
|
|||||||
|
|
||||||
Packet packet = packetMap.getOrDefault(type, null);
|
Packet packet = packetMap.getOrDefault(type, null);
|
||||||
|
|
||||||
if (packet == null) return null;
|
if (packet == null) {
|
||||||
|
packetMetrics.recordUnrecognisedPacket();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return packet.readBytes(ByteBuffer.wrap(stream.readNBytes(size)), size);
|
Packet result = packet.readBytes(ByteBuffer.wrap(stream.readNBytes(size)), size);
|
||||||
|
packetMetrics.recordReceivedPacket(result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writePacket(BufferedOutputStream stream, Packet packet) throws IOException {
|
public void writePacket(BufferedOutputStream stream, Packet packet) throws IOException {
|
||||||
@ -82,6 +97,8 @@ public class PacketManager {
|
|||||||
packet.writeBytes(buffer);
|
packet.writeBytes(buffer);
|
||||||
|
|
||||||
stream.write(buffer.array());
|
stream.write(buffer.array());
|
||||||
|
|
||||||
|
packetMetrics.recordSentPacket(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writePacket(OutputStream stream, Packet packet) throws IOException {
|
public void writePacket(OutputStream stream, Packet packet) throws IOException {
|
||||||
|
|||||||
74
Lib/src/main/java/dev/wiing/gossip/lib/PacketMetrics.java
Normal file
74
Lib/src/main/java/dev/wiing/gossip/lib/PacketMetrics.java
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package dev.wiing.gossip.lib;
|
||||||
|
|
||||||
|
import dev.wiing.gossip.lib.packets.Packet;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class PacketMetrics {
|
||||||
|
|
||||||
|
private final Map<Class<Packet>, List<Packet>> sendHistory = new ConcurrentHashMap<>();
|
||||||
|
private int totalPacketsSentCount = 0;
|
||||||
|
private long totalPacketsSentBytes = 0;
|
||||||
|
|
||||||
|
private final Map<Class<Packet>, List<Packet>> receiveHistory = new ConcurrentHashMap<>();
|
||||||
|
private int totalPacketsReceivedCount = 0;
|
||||||
|
private long totalPacketsReceivedBytes = 0;
|
||||||
|
|
||||||
|
private int totalPacketsUnrecognised = 0;
|
||||||
|
|
||||||
|
public PacketMetrics() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordSentPacket(Packet packet) {
|
||||||
|
if (!sendHistory.containsKey(packet.getClass())) {
|
||||||
|
sendHistory.put((Class<Packet>) packet.getClass(), Collections.synchronizedList(new ArrayList<>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
sendHistory.get(packet.getClass()).add(packet);
|
||||||
|
++totalPacketsSentCount;
|
||||||
|
totalPacketsSentBytes += packet.getTotalLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordReceivedPacket(Packet packet) {
|
||||||
|
if (!receiveHistory.containsKey(packet.getClass())) {
|
||||||
|
receiveHistory.put((Class<Packet>) packet.getClass(), Collections.synchronizedList(new ArrayList<>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
receiveHistory.get(packet.getClass()).add(packet);
|
||||||
|
++totalPacketsReceivedCount;
|
||||||
|
totalPacketsReceivedBytes += packet.getTotalLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordUnrecognisedPacket() {
|
||||||
|
++totalPacketsUnrecognised;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Class<Packet>, List<Packet>> getSendHistory() {
|
||||||
|
return Collections.unmodifiableMap(sendHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalPacketsSentCount() {
|
||||||
|
return totalPacketsSentCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalPacketsSentBytes() {
|
||||||
|
return totalPacketsSentBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Class<Packet>, List<Packet>> getReceiveHistory() {
|
||||||
|
return Collections.unmodifiableMap(receiveHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalPacketsReceivedCount() {
|
||||||
|
return totalPacketsReceivedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalPacketsReceivedBytes() {
|
||||||
|
return totalPacketsReceivedBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,6 +18,8 @@ public class Topic {
|
|||||||
private final List<Message> messages = Collections.synchronizedList(new ArrayList<>());
|
private final List<Message> messages = Collections.synchronizedList(new ArrayList<>());
|
||||||
private final Map<Long, Message> messageByIDs = new ConcurrentHashMap<>();
|
private final Map<Long, Message> messageByIDs = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final Set<User> typingUsers = Collections.synchronizedSet(new HashSet<>());
|
||||||
|
|
||||||
private long messageTrackerID = 1;
|
private long messageTrackerID = 1;
|
||||||
|
|
||||||
public Topic(long id, String name, String description, User host, short color) {
|
public Topic(long id, String name, String description, User host, short color) {
|
||||||
@ -101,6 +103,30 @@ public class Topic {
|
|||||||
this.changeSupport.firePropertyChange("messageAdd", null, message);
|
this.changeSupport.firePropertyChange("messageAdd", null, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<User> getTypingUsersReadOnly() {
|
||||||
|
return Collections.unmodifiableSet(typingUsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTypingUsersCount() {
|
||||||
|
return typingUsers.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addTypingUser(User user) {
|
||||||
|
if (typingUsers.contains(user)) return;
|
||||||
|
|
||||||
|
typingUsers.add(user);
|
||||||
|
|
||||||
|
this.changeSupport.firePropertyChange("typingAdd", null, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeTypingUser(User user) {
|
||||||
|
if (!typingUsers.contains(user)) return;
|
||||||
|
|
||||||
|
typingUsers.remove(user);
|
||||||
|
|
||||||
|
this.changeSupport.firePropertyChange("typingRemove", null, user);
|
||||||
|
}
|
||||||
|
|
||||||
public Message getMessageByID(long messageID) {
|
public Message getMessageByID(long messageID) {
|
||||||
return messageByIDs.getOrDefault(messageID, null);
|
return messageByIDs.getOrDefault(messageID, null);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,59 @@
|
|||||||
|
package dev.wiing.gossip.lib.packets;
|
||||||
|
|
||||||
|
import dev.wiing.gossip.lib.data.ListData;
|
||||||
|
import dev.wiing.gossip.lib.data.LongData;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TypingListUpdatePacket extends Packet {
|
||||||
|
|
||||||
|
public static final short TYPE = 0x42;
|
||||||
|
public static final int LENGTH = 0x000;
|
||||||
|
|
||||||
|
private long topicID;
|
||||||
|
private final ListData<LongData> typingMembers = new ListData<>(LongData::new);
|
||||||
|
|
||||||
|
public TypingListUpdatePacket() {
|
||||||
|
super(TYPE, LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLength() {
|
||||||
|
setLength(8 + typingMembers.getLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getLength() {
|
||||||
|
updateLength();
|
||||||
|
return super.getLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTopicID() {
|
||||||
|
return topicID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTopicID(long topicID) {
|
||||||
|
this.topicID = topicID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<LongData> getTypingMembers() {
|
||||||
|
return typingMembers.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Packet readBytes(ByteBuffer buffer, int size) {
|
||||||
|
TypingListUpdatePacket packet = new TypingListUpdatePacket();
|
||||||
|
packet.setLength(size);
|
||||||
|
|
||||||
|
packet.topicID = buffer.getLong();
|
||||||
|
packet.typingMembers.setBytes(buffer);
|
||||||
|
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeBytes(ByteBuffer buffer) {
|
||||||
|
buffer.putLong(topicID);
|
||||||
|
buffer.put(typingMembers.getBytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package dev.wiing.gossip.lib.packets;
|
||||||
|
|
||||||
|
import dev.wiing.gossip.lib.data.AuthSecret;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class TypingPingPacket extends AuthRequiredPacket {
|
||||||
|
|
||||||
|
public static final short TYPE = 0x41;
|
||||||
|
public static final int LENGTH = 0x029;
|
||||||
|
|
||||||
|
private long topicID;
|
||||||
|
private boolean typing;
|
||||||
|
|
||||||
|
public TypingPingPacket() {
|
||||||
|
super(TYPE, LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTopicID() {
|
||||||
|
return topicID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTopicID(long topicID) {
|
||||||
|
this.topicID = topicID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTyping() {
|
||||||
|
return typing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTyping(boolean typing) {
|
||||||
|
this.typing = typing;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Packet readBytes(ByteBuffer buffer, int size) {
|
||||||
|
TypingPingPacket packet = new TypingPingPacket();
|
||||||
|
|
||||||
|
packet.setAuth(new AuthSecret(buffer));
|
||||||
|
packet.topicID = buffer.getLong();
|
||||||
|
packet.typing = buffer.get() > 0;
|
||||||
|
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeBytes(ByteBuffer buffer) {
|
||||||
|
buffer.put(getAuth().getBytes());
|
||||||
|
buffer.putLong(topicID);
|
||||||
|
buffer.put(typing ? (byte)1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,9 +4,11 @@ import dev.wiing.gossip.lib.data.AuthSecret;
|
|||||||
import dev.wiing.gossip.lib.models.SecretUser;
|
import dev.wiing.gossip.lib.models.SecretUser;
|
||||||
import dev.wiing.gossip.lib.models.Topic;
|
import dev.wiing.gossip.lib.models.Topic;
|
||||||
import dev.wiing.gossip.lib.models.User;
|
import dev.wiing.gossip.lib.models.User;
|
||||||
|
import dev.wiing.gossip.server.customs.TypingTopic;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@ -15,14 +17,14 @@ public class Database {
|
|||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(Database.class);
|
private static final Logger logger = LogManager.getLogger(Database.class);
|
||||||
|
|
||||||
private int userIdCounter = 1;
|
private int channelIDCounter = 1;
|
||||||
private final Map<Long, User> users = Collections.synchronizedMap(new HashMap<>());
|
private final Map<Long, User> users = Collections.synchronizedMap(new HashMap<>());
|
||||||
private final Map<String, User> usersBySecret = Collections.synchronizedMap(new HashMap<>());
|
private final Map<String, User> usersBySecret = Collections.synchronizedMap(new HashMap<>());
|
||||||
private final Map<Long, Socket> userSockets = Collections.synchronizedMap(new HashMap<>());
|
private final Map<Long, Socket> userSockets = Collections.synchronizedMap(new HashMap<>());
|
||||||
private final Set<String> usedUsernames = Collections.synchronizedSet(new HashSet<>());
|
private final Set<String> usedUsernames = Collections.synchronizedSet(new HashSet<>());
|
||||||
|
|
||||||
private int topicIdCounter = 1;
|
private final Map<Socket, User> connectedUsers = Collections.synchronizedMap(new HashMap<>());
|
||||||
private final Map<Long, Topic> topics = Collections.synchronizedMap(new HashMap<>());
|
private final Map<Long, TypingTopic> topics = Collections.synchronizedMap(new HashMap<>());
|
||||||
|
|
||||||
private Database() {
|
private Database() {
|
||||||
|
|
||||||
@ -41,7 +43,7 @@ public class Database {
|
|||||||
random.nextBytes(secretBytes);
|
random.nextBytes(secretBytes);
|
||||||
AuthSecret secret = new AuthSecret(secretBytes);
|
AuthSecret secret = new AuthSecret(secretBytes);
|
||||||
|
|
||||||
long userID = userIdCounter++;
|
long userID = channelIDCounter++;
|
||||||
|
|
||||||
User user = new User(username, iconID, userID);
|
User user = new User(username, iconID, userID);
|
||||||
|
|
||||||
@ -49,6 +51,7 @@ public class Database {
|
|||||||
userSockets.put(userID, socket);
|
userSockets.put(userID, socket);
|
||||||
usersBySecret.put(secret.getString(), user);
|
usersBySecret.put(secret.getString(), user);
|
||||||
usedUsernames.add(username);
|
usedUsernames.add(username);
|
||||||
|
connectedUsers.put(socket, user);
|
||||||
|
|
||||||
logger.info("User created: \"{}\" (#{})", user.getUsername(), user.getUserID());
|
logger.info("User created: \"{}\" (#{})", user.getUsername(), user.getUserID());
|
||||||
|
|
||||||
@ -79,12 +82,7 @@ public class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public User getUserOfSocket(Socket socket) {
|
public User getUserOfSocket(Socket socket) {
|
||||||
return userSockets.entrySet().stream()
|
return connectedUsers.get(socket);
|
||||||
.filter(entry -> entry.getValue().equals(socket))
|
|
||||||
.findFirst()
|
|
||||||
.map(Map.Entry::getKey)
|
|
||||||
.map(this::getUserByID)
|
|
||||||
.orElse(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeUser(User user) {
|
public void removeUser(User user) {
|
||||||
@ -96,22 +94,43 @@ public class Database {
|
|||||||
.findFirst()
|
.findFirst()
|
||||||
.ifPresent(usersBySecret::remove);
|
.ifPresent(usersBySecret::remove);
|
||||||
|
|
||||||
userSockets.remove(user.getUserID());
|
|
||||||
|
|
||||||
usedUsernames.remove(user.getUsername());
|
usedUsernames.remove(user.getUsername());
|
||||||
|
|
||||||
|
disconnectUser(user);
|
||||||
|
|
||||||
logger.info("User removed: \"{}\" (#{})", user.getUsername(), user.getUserID());
|
logger.info("User removed: \"{}\" (#{})", user.getUsername(), user.getUserID());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Topic createTopic(AuthSecret userSecret, String topicName, String topicDescription) {
|
public void disconnectUser(User user) {
|
||||||
|
Socket socket = null;
|
||||||
|
|
||||||
|
if (userSockets.containsKey(user.getUserID())) {
|
||||||
|
socket = userSockets.remove(user.getUserID());
|
||||||
|
|
||||||
|
if (socket != null && !socket.isClosed()) {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socket != null) {
|
||||||
|
connectedUsers.remove(socket);
|
||||||
|
|
||||||
|
logger.info("User disconnected: \"{}\" (#{})", user.getUsername(), user.getUserID());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TypingTopic createTopic(AuthSecret userSecret, String topicName, String topicDescription) {
|
||||||
if (!usersBySecret.containsKey(userSecret.getString())) return null;
|
if (!usersBySecret.containsKey(userSecret.getString())) return null;
|
||||||
|
|
||||||
User user = usersBySecret.get(userSecret.getString());
|
User user = usersBySecret.get(userSecret.getString());
|
||||||
|
|
||||||
short colorHue = (short)Math.abs((new Random().nextInt(360)));
|
short colorHue = (short)Math.abs((new Random().nextInt(360)));
|
||||||
|
|
||||||
Topic topic = new Topic(
|
TypingTopic topic = new TypingTopic(
|
||||||
topicIdCounter++,
|
channelIDCounter++,
|
||||||
topicName,
|
topicName,
|
||||||
topicDescription,
|
topicDescription,
|
||||||
user,
|
user,
|
||||||
@ -125,17 +144,17 @@ public class Database {
|
|||||||
return topic;
|
return topic;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Topic getTopic(long topicID) {
|
public TypingTopic getTopic(long topicID) {
|
||||||
return topics.getOrDefault(topicID, null);
|
return topics.getOrDefault(topicID, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeTopic(long topicID) {
|
public void removeTopic(long topicID) {
|
||||||
Topic topic = topics.remove(topicID);
|
TypingTopic topic = topics.remove(topicID);
|
||||||
|
|
||||||
logger.info("Topic removed: \"{}\" (#{})", topic.getName(), topic.getId());
|
logger.info("Topic removed: \"{}\" (#{})", topic.getName(), topic.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Topic> getAllTopicsReadOnly() {
|
public List<TypingTopic> getAllTopicsReadOnly() {
|
||||||
return Collections.unmodifiableList(topics.values().stream().toList());
|
return topics.values().stream().toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,13 +3,13 @@ package dev.wiing.gossip.server;
|
|||||||
import dev.wiing.gossip.lib.data.LongData;
|
import dev.wiing.gossip.lib.data.LongData;
|
||||||
import dev.wiing.gossip.lib.models.*;
|
import dev.wiing.gossip.lib.models.*;
|
||||||
import dev.wiing.gossip.lib.packets.*;
|
import dev.wiing.gossip.lib.packets.*;
|
||||||
|
import dev.wiing.gossip.server.customs.TypingTopic;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public record UserSocket(Socket socket) implements Runnable {
|
public record UserSocket(Socket socket) implements Runnable {
|
||||||
|
|
||||||
@ -72,6 +72,10 @@ public record UserSocket(Socket socket) implements Runnable {
|
|||||||
case MessageFetchPacket.TYPE:
|
case MessageFetchPacket.TYPE:
|
||||||
onFetchMessage((MessageFetchPacket) packet);
|
onFetchMessage((MessageFetchPacket) packet);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TypingPingPacket.TYPE:
|
||||||
|
onTypingUser((TypingPingPacket) packet);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} catch (SocketException e) {
|
} catch (SocketException e) {
|
||||||
loop = false;
|
loop = false;
|
||||||
@ -82,7 +86,7 @@ public record UserSocket(Socket socket) implements Runnable {
|
|||||||
|
|
||||||
// Connection lost
|
// Connection lost
|
||||||
User user = Database.getInstance().getUserOfSocket(socket);
|
User user = Database.getInstance().getUserOfSocket(socket);
|
||||||
Database.getInstance().removeUser(user);
|
Database.getInstance().disconnectUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onRegisterUser(RegisterRequestPacket packet) {
|
private void onRegisterUser(RegisterRequestPacket packet) {
|
||||||
@ -118,6 +122,8 @@ public record UserSocket(Socket socket) implements Runnable {
|
|||||||
for (User user : Database.getInstance().getUsers()) {
|
for (User user : Database.getInstance().getUsers()) {
|
||||||
Socket socket = Database.getInstance().getUserSocket(user.getUserID());
|
Socket socket = Database.getInstance().getUserSocket(user.getUserID());
|
||||||
|
|
||||||
|
if (socket == null) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Globals.getPacketManager().writePacket(socket.getOutputStream(), added);
|
Globals.getPacketManager().writePacket(socket.getOutputStream(), added);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -162,6 +168,8 @@ public record UserSocket(Socket socket) implements Runnable {
|
|||||||
for (User user : Database.getInstance().getUsers()) {
|
for (User user : Database.getInstance().getUsers()) {
|
||||||
Socket socket = Database.getInstance().getUserSocket(user.getUserID());
|
Socket socket = Database.getInstance().getUserSocket(user.getUserID());
|
||||||
|
|
||||||
|
if (socket == null) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Globals.getPacketManager().writeAllPackets(socket.getOutputStream(), updatePacket, systemPacket);
|
Globals.getPacketManager().writeAllPackets(socket.getOutputStream(), updatePacket, systemPacket);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -259,6 +267,8 @@ public record UserSocket(Socket socket) implements Runnable {
|
|||||||
for (User babbler : topic.getUsersReadOnly().values()) {
|
for (User babbler : topic.getUsersReadOnly().values()) {
|
||||||
Socket userSocket = Database.getInstance().getUserSocket(babbler.getUserID());
|
Socket userSocket = Database.getInstance().getUserSocket(babbler.getUserID());
|
||||||
|
|
||||||
|
if (userSocket == null) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Globals.getPacketManager().writePacket(userSocket.getOutputStream(), messageCreated);
|
Globals.getPacketManager().writePacket(userSocket.getOutputStream(), messageCreated);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -306,6 +316,7 @@ public record UserSocket(Socket socket) implements Runnable {
|
|||||||
resp.setMessageType(MessageDataPacket.MessageType.USER);
|
resp.setMessageType(MessageDataPacket.MessageType.USER);
|
||||||
resp.setUserAuthorID(userMessage.getAuthor().getUserID());
|
resp.setUserAuthorID(userMessage.getAuthor().getUserID());
|
||||||
resp.setUserContents(userMessage.getContents());
|
resp.setUserContents(userMessage.getContents());
|
||||||
|
|
||||||
} else if (message instanceof SystemMessage systemMessage) {
|
} else if (message instanceof SystemMessage systemMessage) {
|
||||||
resp.setMessageType(MessageDataPacket.MessageType.SYSTEM);
|
resp.setMessageType(MessageDataPacket.MessageType.SYSTEM);
|
||||||
resp.setSystemType(systemMessage.getType());
|
resp.setSystemType(systemMessage.getType());
|
||||||
@ -321,4 +332,68 @@ public record UserSocket(Socket socket) implements Runnable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onTypingUser(TypingPingPacket packet) {
|
||||||
|
User user = Database.getInstance().getUserBySecret(packet.getAuth());
|
||||||
|
|
||||||
|
if (user == null) return;
|
||||||
|
|
||||||
|
TypingTopic topic = Database.getInstance().getTopic(packet.getTopicID());
|
||||||
|
|
||||||
|
if (topic == null || !topic.hasUser(user)) return;
|
||||||
|
|
||||||
|
if (packet.isTyping()) {
|
||||||
|
topic.addTypingUser(user);
|
||||||
|
|
||||||
|
if (topic.getTypingExpiry().containsKey(user)) topic.getTypingExpiry().get(user).interrupt();
|
||||||
|
|
||||||
|
Thread thread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(20_000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
topic.removeTypingUser(user);
|
||||||
|
|
||||||
|
announceTypingList(topic);
|
||||||
|
|
||||||
|
topic.getTypingExpiry().remove(user);
|
||||||
|
});
|
||||||
|
thread.start();
|
||||||
|
|
||||||
|
topic.getTypingExpiry().put(user, thread);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
topic.removeTypingUser(user);
|
||||||
|
|
||||||
|
Thread thread = topic.getTypingExpiry().remove(user);
|
||||||
|
if (thread != null && thread.isAlive()) thread.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
info("User #{} typing ping on #{}", user.getUserID(), topic.getId());
|
||||||
|
|
||||||
|
announceTypingList(topic);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void announceTypingList(TypingTopic topic) {
|
||||||
|
TypingListUpdatePacket resp = new TypingListUpdatePacket();
|
||||||
|
resp.setTopicID(topic.getId());
|
||||||
|
resp.getTypingMembers().addAll(topic.getTypingUsersReadOnly().stream()
|
||||||
|
.map(typing -> new LongData().setValue(typing.getUserID()))
|
||||||
|
.toList());
|
||||||
|
|
||||||
|
for (User babbler : topic.getUsersReadOnly().values()) {
|
||||||
|
Socket userSocket = Database.getInstance().getUserSocket(babbler.getUserID());
|
||||||
|
|
||||||
|
if (userSocket == null) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Globals.getPacketManager().writePacket(userSocket.getOutputStream(), resp);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
package dev.wiing.gossip.server.customs;
|
||||||
|
|
||||||
|
import dev.wiing.gossip.lib.models.Topic;
|
||||||
|
import dev.wiing.gossip.lib.models.User;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class TypingTopic extends Topic {
|
||||||
|
|
||||||
|
private final Map<User, Thread> typingExpiry = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public TypingTopic(long id, String name, String description, User host, short color) {
|
||||||
|
super(id, name, description, host, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<User, Thread> getTypingExpiry() {
|
||||||
|
return typingExpiry;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user