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.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>
|
||||
|
||||
@ -8,8 +8,23 @@ import java.util.stream.Collectors;
|
||||
|
||||
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<>();
|
||||
|
||||
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) {
|
||||
if (!listeners.containsKey(type)) {
|
||||
listeners.put(type, new HashSet<>());
|
||||
@ -39,35 +54,25 @@ public class PacketHandler {
|
||||
}
|
||||
|
||||
public boolean runPacket(Packet packet) {
|
||||
return switch (packet.getType()) {
|
||||
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;
|
||||
};
|
||||
return runPacket((Class<Packet>)packet.getClass(), packet);
|
||||
}
|
||||
|
||||
public <T extends Packet> Set<PacketListener<T>> getListeners(Class<T> packetClass) {
|
||||
if (!listeners.containsKey(packetClass))
|
||||
return null;
|
||||
|
||||
return listeners.get(packetClass).stream().map(listener -> {
|
||||
return (PacketListener<T>) listener;
|
||||
}).collect(Collectors.toSet());
|
||||
return listeners.get(packetClass).stream()
|
||||
.map(listener -> (PacketListener<T>) listener)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public List<Short> getListeningTypes() {
|
||||
return new ArrayList<>() {
|
||||
{
|
||||
add(TopicCreatedPacket.TYPE);
|
||||
add(TopicUpdatePacket.TYPE);
|
||||
add(TopicListDataPacket.TYPE);
|
||||
add(MessageCreatedPacket.TYPE);
|
||||
add(MessageSystemPacket.TYPE);
|
||||
}
|
||||
};
|
||||
return listeners.entrySet().stream()
|
||||
.filter(classSetEntry -> !classSetEntry.getValue().isEmpty())
|
||||
.map(Map.Entry::getKey)
|
||||
.map(packetClassMap::get)
|
||||
.map(Packet::getType)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -4,10 +4,7 @@ import dev.wiing.gossip.lib.packets.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
public class PacketManager {
|
||||
|
||||
@ -38,9 +35,14 @@ public class PacketManager {
|
||||
add(new MessageListDataPacket());
|
||||
add(new MessageFetchPacket());
|
||||
add(new MessageDataPacket());
|
||||
|
||||
add(new TypingPingPacket());
|
||||
add(new TypingListUpdatePacket());
|
||||
}
|
||||
};
|
||||
|
||||
private final PacketMetrics packetMetrics = new PacketMetrics();
|
||||
|
||||
private final Map<Short, Packet> packetMap = new HashMap<>();
|
||||
|
||||
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 {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(stream.readNBytes(6));
|
||||
|
||||
@ -68,9 +78,14 @@ public class PacketManager {
|
||||
|
||||
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 {
|
||||
@ -82,6 +97,8 @@ public class PacketManager {
|
||||
packet.writeBytes(buffer);
|
||||
|
||||
stream.write(buffer.array());
|
||||
|
||||
packetMetrics.recordSentPacket(packet);
|
||||
}
|
||||
|
||||
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 Map<Long, Message> messageByIDs = new ConcurrentHashMap<>();
|
||||
|
||||
private final Set<User> typingUsers = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
private long messageTrackerID = 1;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
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.Topic;
|
||||
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.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
@ -15,14 +17,14 @@ public class Database {
|
||||
|
||||
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<String, User> usersBySecret = Collections.synchronizedMap(new HashMap<>());
|
||||
private final Map<Long, Socket> userSockets = Collections.synchronizedMap(new HashMap<>());
|
||||
private final Set<String> usedUsernames = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
private int topicIdCounter = 1;
|
||||
private final Map<Long, Topic> topics = Collections.synchronizedMap(new HashMap<>());
|
||||
private final Map<Socket, User> connectedUsers = Collections.synchronizedMap(new HashMap<>());
|
||||
private final Map<Long, TypingTopic> topics = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private Database() {
|
||||
|
||||
@ -41,7 +43,7 @@ public class Database {
|
||||
random.nextBytes(secretBytes);
|
||||
AuthSecret secret = new AuthSecret(secretBytes);
|
||||
|
||||
long userID = userIdCounter++;
|
||||
long userID = channelIDCounter++;
|
||||
|
||||
User user = new User(username, iconID, userID);
|
||||
|
||||
@ -49,6 +51,7 @@ public class Database {
|
||||
userSockets.put(userID, socket);
|
||||
usersBySecret.put(secret.getString(), user);
|
||||
usedUsernames.add(username);
|
||||
connectedUsers.put(socket, user);
|
||||
|
||||
logger.info("User created: \"{}\" (#{})", user.getUsername(), user.getUserID());
|
||||
|
||||
@ -79,12 +82,7 @@ public class Database {
|
||||
}
|
||||
|
||||
public User getUserOfSocket(Socket socket) {
|
||||
return userSockets.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().equals(socket))
|
||||
.findFirst()
|
||||
.map(Map.Entry::getKey)
|
||||
.map(this::getUserByID)
|
||||
.orElse(null);
|
||||
return connectedUsers.get(socket);
|
||||
}
|
||||
|
||||
public void removeUser(User user) {
|
||||
@ -96,22 +94,43 @@ public class Database {
|
||||
.findFirst()
|
||||
.ifPresent(usersBySecret::remove);
|
||||
|
||||
userSockets.remove(user.getUserID());
|
||||
|
||||
usedUsernames.remove(user.getUsername());
|
||||
|
||||
disconnectUser(user);
|
||||
|
||||
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;
|
||||
|
||||
User user = usersBySecret.get(userSecret.getString());
|
||||
|
||||
short colorHue = (short)Math.abs((new Random().nextInt(360)));
|
||||
|
||||
Topic topic = new Topic(
|
||||
topicIdCounter++,
|
||||
TypingTopic topic = new TypingTopic(
|
||||
channelIDCounter++,
|
||||
topicName,
|
||||
topicDescription,
|
||||
user,
|
||||
@ -125,17 +144,17 @@ public class Database {
|
||||
return topic;
|
||||
}
|
||||
|
||||
public Topic getTopic(long topicID) {
|
||||
public TypingTopic getTopic(long topicID) {
|
||||
return topics.getOrDefault(topicID, null);
|
||||
}
|
||||
|
||||
public void removeTopic(long topicID) {
|
||||
Topic topic = topics.remove(topicID);
|
||||
TypingTopic topic = topics.remove(topicID);
|
||||
|
||||
logger.info("Topic removed: \"{}\" (#{})", topic.getName(), topic.getId());
|
||||
}
|
||||
|
||||
public List<Topic> getAllTopicsReadOnly() {
|
||||
return Collections.unmodifiableList(topics.values().stream().toList());
|
||||
public List<TypingTopic> getAllTopicsReadOnly() {
|
||||
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.models.*;
|
||||
import dev.wiing.gossip.lib.packets.*;
|
||||
import dev.wiing.gossip.server.customs.TypingTopic;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.List;
|
||||
|
||||
public record UserSocket(Socket socket) implements Runnable {
|
||||
|
||||
@ -72,6 +72,10 @@ public record UserSocket(Socket socket) implements Runnable {
|
||||
case MessageFetchPacket.TYPE:
|
||||
onFetchMessage((MessageFetchPacket) packet);
|
||||
break;
|
||||
|
||||
case TypingPingPacket.TYPE:
|
||||
onTypingUser((TypingPingPacket) packet);
|
||||
break;
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
loop = false;
|
||||
@ -82,7 +86,7 @@ public record UserSocket(Socket socket) implements Runnable {
|
||||
|
||||
// Connection lost
|
||||
User user = Database.getInstance().getUserOfSocket(socket);
|
||||
Database.getInstance().removeUser(user);
|
||||
Database.getInstance().disconnectUser(user);
|
||||
}
|
||||
|
||||
private void onRegisterUser(RegisterRequestPacket packet) {
|
||||
@ -118,6 +122,8 @@ public record UserSocket(Socket socket) implements Runnable {
|
||||
for (User user : Database.getInstance().getUsers()) {
|
||||
Socket socket = Database.getInstance().getUserSocket(user.getUserID());
|
||||
|
||||
if (socket == null) continue;
|
||||
|
||||
try {
|
||||
Globals.getPacketManager().writePacket(socket.getOutputStream(), added);
|
||||
} catch (IOException e) {
|
||||
@ -162,6 +168,8 @@ public record UserSocket(Socket socket) implements Runnable {
|
||||
for (User user : Database.getInstance().getUsers()) {
|
||||
Socket socket = Database.getInstance().getUserSocket(user.getUserID());
|
||||
|
||||
if (socket == null) continue;
|
||||
|
||||
try {
|
||||
Globals.getPacketManager().writeAllPackets(socket.getOutputStream(), updatePacket, systemPacket);
|
||||
} catch (IOException e) {
|
||||
@ -259,6 +267,8 @@ public record UserSocket(Socket socket) implements Runnable {
|
||||
for (User babbler : topic.getUsersReadOnly().values()) {
|
||||
Socket userSocket = Database.getInstance().getUserSocket(babbler.getUserID());
|
||||
|
||||
if (userSocket == null) continue;
|
||||
|
||||
try {
|
||||
Globals.getPacketManager().writePacket(userSocket.getOutputStream(), messageCreated);
|
||||
} catch (IOException e) {
|
||||
@ -306,6 +316,7 @@ public record UserSocket(Socket socket) implements Runnable {
|
||||
resp.setMessageType(MessageDataPacket.MessageType.USER);
|
||||
resp.setUserAuthorID(userMessage.getAuthor().getUserID());
|
||||
resp.setUserContents(userMessage.getContents());
|
||||
|
||||
} else if (message instanceof SystemMessage systemMessage) {
|
||||
resp.setMessageType(MessageDataPacket.MessageType.SYSTEM);
|
||||
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