Entrega v1 aula: shell com abas, PITIUPI_HOME e plugins de referência.

Consolida núcleo (presença, peer actions, MessageDispatcher), chat e screencast na galeria, documentação PT-BR e testes JUnit; remove mpChat do catálogo em favor do chat unificado.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Gabriel Felipe Souza SantosandCursor committed 2026-10-27 11:08:21 -03:00
1 parent b10c196fc9
commit eeb1936f6d
178 files changed
+7641 -1481

No files matched your search

@@ -0,0 +1 @@
pitiupi.chat.plugin.ChatPlugin
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../../pitiupi/dist/pitiupi.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
@@ -0,0 +1,32 @@
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package pitiupi.chat.GUI;
import pitiupi.models.Menus.MulticastMenus.MulticastMenu;
import pitiupi.models.Menus.MulticastMenus.MulticastSubMenu;
import java.util.List;
/**
*
* @author flavio
*/
public class ChatMenu extends MulticastSubMenu {
ChatMulticastMenu chatMulticastMenu;
public ChatMenu(ChatWindow chatWindow) {
this.chatMulticastMenu = new ChatMulticastMenu(chatWindow);
}
@Override
public String getText() {
return "Chat";
}
@Override
public List<MulticastMenu> getMenus() {
return List.of(chatMulticastMenu);
}
}
@@ -0,0 +1,21 @@
package pitiupi.chat.GUI;
import pitiupi.models.Menus.MulticastMenus.MulticastActionMenu;;
public class ChatMulticastMenu extends MulticastActionMenu {
ChatWindow chatWindow;
public ChatMulticastMenu(ChatWindow chatWindow) {
this.chatWindow = chatWindow;
}
@Override
public String getText() {
return "Show Chat Window";
}
@Override
public void onClick() {
chatWindow.setVisible(true);
}
}
@@ -0,0 +1,633 @@
package pitiupi.chat.GUI;
import pitiupi.application.messaging.MessageDispatcher;
import pitiupi.application.messaging.PluginTransportCatalog;
import pitiupi.application.messaging.SendTarget;
import pitiupi.application.messaging.TransportMode;
import pitiupi.application.presence.UserPeerView;
import pitiupi.chat.control.PrivateChatController;
import pitiupi.chat.control.PrivateChatUiSink;
import pitiupi.chat.net.ChatMessage;
import pitiupi.chat.net.LegacyMpChatMessage;
import pitiupi.control.ConnectionManager;
import pitiupi.models.User;
import pitiupi.presentation.shell.LocalNetworkInfo;
import pitiupi.presentation.shell.LocalUserIdentity;
import pitiupi.presentation.shell.PeerDraftPanel;
import pitiupi.presentation.shell.PluginRoutingPanel;
import pitiupi.presentation.shell.PluginSessionKeys;
import pitiupi.presentation.shell.PluginSessionWorkspace;
import pitiupi.presentation.shell.SessionPeerLabels;
import pitiupi.presentation.shell.SessionTabBar;
import pitiupi.presentation.shell.ui.ShellUi;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JEditorPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.AbstractAction;
import javax.swing.KeyStroke;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import java.awt.Dimension;
import java.awt.event.HierarchyEvent;
import javax.swing.text.html.HTMLEditorKit;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.net.InetAddress;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class ChatPanel extends JPanel implements PrivateChatUiSink {
private static final String ROOM_KEY = "room";
private static final String CARD_MAIN = "main";
private static final String CARD_DRAFT = "draft";
private final JEditorPane transcript = new JEditorPane();
private final JTextArea input = new JTextArea();
private final ConnectionManager connectionManager;
private final PluginSessionWorkspace workspace;
private final PluginRoutingPanel routingPanel;
private final SessionTabBar sessionTabs;
private final CardLayout bodyLayout = new CardLayout();
private final JPanel bodyCards = new JPanel(bodyLayout);
private String lastSessionKey = PluginSessionKeys.ROOM;
private final Map<String, StringBuilder> transcripts = new HashMap<>();
private final Map<String, String> peerDisplayNames = new HashMap<>();
private String activeConversationKey = ROOM_KEY;
private PrivateChatController privateChatController;
private PeerDraftPanel draftPanel;
private Runnable pendingTranscriptFlush;
public ChatPanel(ConnectionManager connectionManager) {
this.connectionManager = connectionManager;
this.routingPanel = new PluginRoutingPanel(
PluginTransportCatalog.PluginKind.CHAT,
"<html><i>Use o IP do peer ou escolha alguém na lista lateral → Conversar. "
+ "Envios usam a porta do login (barra de status); na sala, mesma porta para todos.</i></html>",
true
);
routingPanel.setToolTipText(
"UDP/TCP unicast usa o IP do peer e a porta " + connectionManager.getPort()
+ " (a que você informou ao entrar)."
);
this.workspace = new PluginSessionWorkspace(routingPanel, null);
this.sessionTabs = workspace.getSessionTabBar();
setLayout(new BorderLayout(5, 5));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
buildUi();
initSessionTabs();
routingPanel.addRoutingListener(s -> applyConversationKeyFromRouting());
routingPanel.setPrivateTcpListener(selected -> {
if (selected) {
User user = resolveUserForSingle();
if (user != null && privateChatController != null) {
privateChatController.openChat(user);
}
}
applyConversationKeyFromRouting();
});
selectRoomSession();
}
private void initSessionTabs() {
sessionTabs.addOrSelectTab(
PluginSessionKeys.ROOM,
"Sala geral",
"Sala geral (Todos)",
new JPanel(),
false
);
sessionTabs.setCloseFallbackKey(PluginSessionKeys.ROOM);
sessionTabs.setOnTabSelected(this::onSessionTabSelected);
sessionTabs.setOnAddPeerRequested(this::openDraftPeerTab);
sessionTabs.setOnTabClosed(this::onSessionTabClosed);
}
private void onSessionTabClosed(String key) {
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
if (draftPanel != null) {
draftPanel.clearIpField();
}
SwingUtilities.invokeLater(this::restoreSessionAfterDraftClosed);
}
}
private void applyConversationKeyFromRouting() {
String next = resolveConversationKey();
if (!next.equals(activeConversationKey)) {
switchActiveConversation(next);
}
}
private void onSessionTabSelected(String key) {
if (key == null) {
return;
}
lastSessionKey = key;
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
showBodyCard(CARD_DRAFT);
if (draftPanel != null) {
draftPanel.focusIpField();
}
return;
}
showBodyCard(CARD_MAIN);
if (PluginSessionKeys.ROOM.equals(key)) {
routingPanel.bindRoomSession();
switchActiveConversation(ROOM_KEY);
return;
}
if (key.startsWith("peer:") && !PluginSessionKeys.PEER_DRAFT.equals(key)) {
String ip = key.substring("peer:".length());
String name = peerDisplayNames.getOrDefault(ip, ip);
routingPanel.bindPeerSession(ip, name);
switchActiveConversation(resolveConversationKey());
}
}
private void openDraftPeerTab() {
String previous = sessionTabs.selectedKey();
if (previous != null && !PluginSessionKeys.PEER_DRAFT.equals(previous)) {
lastSessionKey = previous;
}
if (!sessionTabs.hasTab(PluginSessionKeys.PEER_DRAFT)) {
sessionTabs.addOrSelectTab(
PluginSessionKeys.PEER_DRAFT,
"Nova conversa",
"Informe o IP do peer",
new JPanel(),
true
);
} else {
sessionTabs.selectTab(PluginSessionKeys.PEER_DRAFT);
}
showBodyCard(CARD_DRAFT);
if (draftPanel != null) {
draftPanel.focusIpField();
}
}
private void commitDraftIp(String ip) {
if (draftPanel != null) {
draftPanel.clearIpField();
}
if (sessionTabs.hasTab(PluginSessionKeys.PEER_DRAFT)) {
sessionTabs.closeTab(PluginSessionKeys.PEER_DRAFT);
}
showBodyCard(CARD_MAIN);
openPeerTab(canonicalPeerIp(ip), ip);
}
private void syncRoutingFromActiveSessionTab() {
String key = sessionTabs.selectedKey();
if (PluginSessionKeys.ROOM.equals(key)) {
routingPanel.bindRoomSession();
if (!ROOM_KEY.equals(activeConversationKey)) {
switchActiveConversation(ROOM_KEY);
}
return;
}
if (key != null && key.startsWith("peer:") && !PluginSessionKeys.PEER_DRAFT.equals(key)) {
String ip = key.substring("peer:".length());
String name = peerDisplayNames.getOrDefault(ip, ip);
routingPanel.bindPeerSession(ip, name);
String conv = resolveConversationKey();
if (!conv.equals(activeConversationKey)) {
switchActiveConversation(conv);
}
}
}
private void restoreSessionAfterDraftClosed() {
showBodyCard(CARD_MAIN);
String restore = lastSessionKey != null && sessionTabs.hasTab(lastSessionKey)
? lastSessionKey
: PluginSessionKeys.ROOM;
if (PluginSessionKeys.PEER_DRAFT.equals(restore)) {
restore = PluginSessionKeys.ROOM;
}
sessionTabs.selectTabSilently(restore);
if (PluginSessionKeys.ROOM.equals(restore)) {
routingPanel.bindRoomSession();
switchActiveConversation(ROOM_KEY);
} else if (restore.startsWith("peer:")) {
String ip = restore.substring("peer:".length());
String name = peerDisplayNames.getOrDefault(ip, ip);
routingPanel.bindPeerSession(ip, name);
switchActiveConversation(resolveConversationKey());
}
}
private void openPeerTab(String ip, String displayName) {
String canonical = canonicalPeerIp(ip);
String resolved = SessionPeerLabels.resolveDisplayName(connectionManager, canonical, displayName);
peerDisplayNames.put(canonical, resolved);
String tabKey = PluginSessionKeys.peer(canonical);
if (sessionTabs.hasTab(tabKey)) {
sessionTabs.setTabTitle(tabKey, resolved, canonical);
sessionTabs.selectTab(tabKey);
} else {
sessionTabs.addOrSelectTab(tabKey, resolved, canonical, new JPanel(), true);
}
routingPanel.bindPeerSession(canonical, resolved);
switchActiveConversation(resolveConversationKey());
showBodyCard(CARD_MAIN);
}
private static String canonicalPeerIp(String ip) {
if (ip == null || ip.isBlank()) {
return "";
}
try {
return InetAddress.getByName(ip.trim()).getHostAddress();
} catch (Exception ex) {
return ip.trim();
}
}
private void selectRoomSession() {
routingPanel.bindRoomSession();
switchActiveConversation(ROOM_KEY);
showBodyCard(CARD_MAIN);
}
public void setPrivateChatController(PrivateChatController privateChatController) {
this.privateChatController = privateChatController;
}
public void focusGeneralRoom() {
sessionTabs.selectTab(PluginSessionKeys.ROOM);
}
public void focusPeer(UserPeerView peer) {
if (peer == null || peer.isBroadcastTarget() || peer.isSelf()) {
focusGeneralRoom();
return;
}
peer.getInetAddress().ifPresent(addr ->
openPeerTab(addr.getHostAddress(), peer.getDisplayName()));
}
public void enablePrivateTcpForPeer(UserPeerView peer) {
focusPeer(peer);
routingPanel.setPrivateTcpOn(true);
switchActiveConversation(resolveConversationKey());
}
public void deliverIncomingMessage(ChatMessage message) {
if (isEchoFromLocalUser(message)) {
return;
}
String bucket = bucketForIncoming(message);
if (!ROOM_KEY.equals(bucket) && message.getSenderIp() != null && !message.getSenderIp().isBlank()) {
String canonical = canonicalPeerIp(message.getSenderIp());
if (!sessionTabs.hasTab(PluginSessionKeys.peer(canonical))) {
openPeerTab(canonical, message.getName());
}
bucket = peerKey(canonical);
}
appendChatLine(bucket, message);
}
@Override
public void onPrivateChannelReady(String peerIp) {
SwingUtilities.invokeLater(() -> {
appendSystemLine(privateKey(peerIp), "Canal TCP privado conectado com " + peerIp + ".");
if (privateKey(peerIp).equals(activeConversationKey)) {
flushTranscriptToPane();
}
});
}
@Override
public void onPrivateChannelClosed(String peerIp) {
SwingUtilities.invokeLater(() -> {
appendSystemLine(privateKey(peerIp), "Canal TCP privado encerrado (" + peerIp + ").");
if (privateKey(peerIp).equals(activeConversationKey)) {
flushTranscriptToPane();
}
});
}
@Override
public void appendPrivateLine(String peerIp, LegacyMpChatMessage message, boolean outgoing) {
SwingUtilities.invokeLater(() -> appendLegacyLine(privateKey(peerIp), message));
}
private void buildUi() {
JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
split.setResizeWeight(0.84);
split.setOneTouchExpandable(true);
transcript.setContentType("text/html");
transcript.setEditable(false);
input.setColumns(20);
input.setRows(2);
input.setLineWrap(true);
input.setWrapStyleWord(true);
input.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "send");
input.getActionMap().put("send", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
sendMessage();
}
});
JButton send = new JButton("Enviar");
send.setFocusPainted(false);
send.addActionListener(e -> sendMessage());
JScrollPane inputScroll = new JScrollPane(input);
inputScroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
inputScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
ShellUi.borderlessScroll(inputScroll);
JPanel compose = new JPanel(new BorderLayout(0, 4));
compose.setPreferredSize(new Dimension(0, 72));
compose.add(inputScroll, BorderLayout.CENTER);
JPanel actions = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0));
actions.add(send);
compose.add(actions, BorderLayout.SOUTH);
split.setTopComponent(new JScrollPane(transcript));
split.setBottomComponent(compose);
split.addHierarchyListener(e -> {
if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0
&& split.isShowing()
&& split.getHeight() > 0) {
split.setDividerLocation(0.84);
}
});
draftPanel = new PeerDraftPanel(
"Nova conversa por IP",
"Informe o endereço IP do peer. A aba será atualizada com o nome quando a conversa abrir.",
this::commitDraftIp
);
bodyCards.add(split, CARD_MAIN);
bodyCards.add(draftPanel, CARD_DRAFT);
workspace.getBodyHost().add(bodyCards, BorderLayout.CENTER);
add(workspace, BorderLayout.CENTER);
}
private void showBodyCard(String card) {
bodyLayout.show(bodyCards, card);
}
private void sendMessage() {
if (input.getText().trim().isEmpty()) {
return;
}
syncRoutingFromActiveSessionTab();
String text = input.getText();
PluginRoutingPanel.RoutingSnapshot routing = routingPanel.snapshot();
try {
if (routing.privateTcpOn() && !routing.everyoneInRoom()) {
String peerIp = canonicalPeerIp(routing.peerIp());
if (LocalNetworkInfo.isLocalIpv4(peerIp)) {
appendSystemLine(
activeConversationKey,
"Canal TCP privado com você mesmo não está disponível; use sala geral ou UDP unicast."
);
return;
}
User user = resolveUserForSingle();
if (user == null) {
appendSystemLine(activeConversationKey, "Informe o IP do peer para o canal TCP privado.");
return;
}
if (privateChatController != null && privateChatController.sendText(user, text)) {
input.setText("");
return;
}
if (privateChatController != null) {
privateChatController.openChat(user);
}
appendSystemLine(privateKey(user.getAddress().getHostAddress()),
"Canal TCP privado conectando… envie de novo em instantes.");
return;
}
TransportMode mode = routing.transport();
SendTarget target = resolveSendTarget(routing);
if (target == null || mode == null) {
appendSystemLine(activeConversationKey, "Informe um IP válido para enviar a um peer.");
return;
}
String ip = LocalNetworkInfo.bestLocalIpv4();
ChatMessage message = new ChatMessage(
connectionManager.getUserName(),
text,
mode.getLabel(),
ip
);
int sent = MessageDispatcher.send(connectionManager, message, target, mode);
appendChatLine(activeConversationKey, message);
if (!routing.everyoneInRoom()) {
String peerIp = canonicalPeerIp(routing.peerIp());
if (LocalNetworkInfo.isLocalIpv4(peerIp) && sent == 0) {
if (mode == TransportMode.TCP) {
appendSystemLine(
activeConversationKey,
"TCP para o próprio host não é suportado; use sala geral ou UDP unicast."
);
} else {
appendSystemLine(
activeConversationKey,
"Mensagem registrada aqui; eco pela rede pode não voltar no mesmo PC."
);
}
}
}
input.setText("");
} catch (Exception ex) {
appendSystemLine(activeConversationKey, "Erro ao enviar: " + ex.getMessage());
}
}
private SendTarget resolveSendTarget(PluginRoutingPanel.RoutingSnapshot routing) throws Exception {
if (routing.everyoneInRoom()) {
return SendTarget.broadcastAll();
}
String ip = canonicalPeerIp(routing.peerIp());
if (ip == null || ip.isEmpty()) {
return null;
}
return SendTarget.singlePeer(InetAddress.getByName(ip));
}
private User resolveUserForSingle() {
try {
String ip = canonicalPeerIp(routingPanel.snapshot().peerIp());
if (ip == null || ip.isEmpty()) {
return null;
}
InetAddress address = InetAddress.getByName(ip);
String displayName = ip;
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
if (view.getInetAddress().isPresent()
&& view.getInetAddress().get().equals(address)) {
displayName = view.getDisplayName();
return new User(displayName, address, System.currentTimeMillis(), view.getInstanceId());
}
}
return new User(displayName, address, System.currentTimeMillis(), null);
} catch (Exception ignored) {
return null;
}
}
public void receiveChatMessage(ChatMessage message) {
runOnEdt(() -> deliverIncomingMessage(message));
}
public void receiveLegacyMessage(String tipo, String nome, String ip, String text) {
runOnEdt(() -> {
LegacyMpChatMessage legacy = new LegacyMpChatMessage(nome, ip, tipo, text);
String bucket = ip != null && !ip.isBlank() ? privateKey(canonicalPeerIp(ip)) : ROOM_KEY;
appendLegacyLine(bucket, legacy);
});
}
private static void runOnEdt(Runnable action) {
if (SwingUtilities.isEventDispatchThread()) {
action.run();
} else {
SwingUtilities.invokeLater(action);
}
}
private void appendChatLine(String bucket, ChatMessage message) {
SimpleDateFormat dt = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
String tag = message.getTransportLabel() != null ? message.getTransportLabel() : "—";
String ip = message.getSenderIp() != null ? message.getSenderIp() : "—";
String line = dt.format(new Date()) + " " + ChatTranscriptHtml.coloredTransportTag(tag)
+ " <b>" + ChatTranscriptHtml.escape(message.getName()) + "</b> ("
+ ChatTranscriptHtml.escape(ip) + "): " + ChatTranscriptHtml.escape(message.getText());
prependLine(bucket, line);
}
private void appendLegacyLine(String bucket, LegacyMpChatMessage message) {
SimpleDateFormat dt = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
String tag = message.getTipoMensagem() != null ? message.getTipoMensagem() : "Legado";
String line = dt.format(new Date()) + " " + ChatTranscriptHtml.coloredTransportTag(tag)
+ " <b>" + ChatTranscriptHtml.escape(message.getNome()) + "</b> ("
+ ChatTranscriptHtml.escape(message.getIp()) + "): "
+ ChatTranscriptHtml.escape(message.getMessage());
prependLine(bucket, line);
}
private void appendSystemLine(String bucket, String text) {
prependLine(bucket, "<i>" + ChatTranscriptHtml.escape(text) + "</i>");
}
private void prependLine(String bucket, String htmlLine) {
StringBuilder buf = transcripts.computeIfAbsent(bucket, k -> new StringBuilder());
buf.insert(0, htmlLine + "<br>");
if (bucket.equals(activeConversationKey)) {
scheduleFlushTranscriptToPane();
}
}
private void scheduleFlushTranscriptToPane() {
if (pendingTranscriptFlush != null) {
return;
}
pendingTranscriptFlush = () -> {
pendingTranscriptFlush = null;
flushTranscriptToPane();
};
SwingUtilities.invokeLater(pendingTranscriptFlush);
}
private void flushTranscriptToPane() {
StringBuilder buf = transcripts.get(activeConversationKey);
String inner = buf != null ? buf.toString() : "";
String html = ChatTranscriptHtml.wrapDocument(inner);
try {
transcript.setText(html);
} catch (RuntimeException ex) {
resetTranscriptEditor();
try {
transcript.setText(html);
} catch (RuntimeException ignored) {
// evita quebrar a EDT se o HTML continuar inválido
}
}
}
private void resetTranscriptEditor() {
transcript.setEditorKit(new HTMLEditorKit());
transcript.setContentType("text/html");
transcript.setEditable(false);
}
private void switchActiveConversation(String newKey) {
if (newKey == null) {
newKey = ROOM_KEY;
}
if (newKey.equals(activeConversationKey)) {
return;
}
activeConversationKey = newKey;
flushTranscriptToPane();
}
private String resolveConversationKey() {
PluginRoutingPanel.RoutingSnapshot routing = routingPanel.snapshot();
if (routing.everyoneInRoom()) {
return ROOM_KEY;
}
String ip = canonicalPeerIp(routing.peerIp());
if (ip == null || ip.isEmpty()) {
return ROOM_KEY;
}
if (routing.privateTcpOn()) {
return privateKey(ip);
}
return peerKey(ip);
}
private static String peerKey(String ip) {
return "peer:" + ip;
}
private static String privateKey(String ip) {
return "private:" + ip;
}
private String bucketForIncoming(ChatMessage message) {
String tag = message.getTransportLabel();
if (tag != null && tag.toLowerCase().contains("multicast")) {
return ROOM_KEY;
}
String ip = message.getSenderIp();
if (ip == null || ip.isBlank()) {
return ROOM_KEY;
}
return peerKey(canonicalPeerIp(ip));
}
private boolean isEchoFromLocalUser(ChatMessage message) {
if (message == null) {
return false;
}
return LocalUserIdentity.isLocalSender(
connectionManager,
message.getName(),
message.getSenderIp()
);
}
}
@@ -0,0 +1,50 @@
package pitiupi.chat.GUI;
import pitiupi.application.messaging.TransportMode;
/**
* HTML do histórico com cores por transporte.
*/
public final class ChatTranscriptHtml {
private ChatTranscriptHtml() {
}
public static String coloredTransportTag(String transportLabel) {
if (transportLabel == null || transportLabel.isBlank()) {
return "<span style='color:#616161'>[—]</span>";
}
String normalized = transportLabel.trim().toLowerCase();
String color;
if (normalized.contains("privado")) {
color = "#6a1b9a";
} else if (normalized.contains("multicast")) {
color = "#2e7d32";
} else if (normalized.contains("udp")) {
color = "#1565c0";
} else if (normalized.contains("tcp")) {
color = "#e65100";
} else {
color = "#616161";
}
return "<span style='color:" + color + ";font-weight:bold'>[" + escape(transportLabel) + "]</span>";
}
public static String tagForMode(TransportMode mode) {
return coloredTransportTag(mode != null ? mode.getLabel() : "—");
}
public static String escape(String text) {
if (text == null) {
return "";
}
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
}
public static String wrapDocument(String innerHtml) {
if (innerHtml == null || innerHtml.isBlank()) {
return "<html><body style='margin:0'></body></html>";
}
return "<html><body style='margin:0'>" + innerHtml + "</body></html>";
}
}
@@ -0,0 +1,116 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package pitiupi.chat.GUI;
import java.text.SimpleDateFormat;
import java.util.Date;
import pitiupi.chat.net.ChatMessage;
import pitiupi.control.ConnectionManager;
import pitiupi.models.Messages.Message;
import javax.swing.*;
/**
*
* @author flavio
*/
public class ChatWindow extends JDialog {
private JButton jButton1;
private JEditorPane jEditorPane1;
private JPanel jPanel1;
private JPanel jPanel2;
private JScrollPane jScrollPane1;
private JScrollPane jScrollPane2;
private JSplitPane jSplitPane1;
private JTextArea jTextArea1;
private final ConnectionManager connectionManager;
public ChatWindow(ConnectionManager connectionManager) {
this.connectionManager = connectionManager;
initComponents();
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
private void initComponents() {
this.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
this.setTitle("Chat Window");
this.setBounds(new java.awt.Rectangle(0, 0, 800, 600));
this.getContentPane().setLayout(new java.awt.BorderLayout(5, 5));
this.setPreferredSize(new java.awt.Dimension(800, 600));
jSplitPane1 = new JSplitPane();
jScrollPane1 = new JScrollPane();
jEditorPane1 = new JEditorPane();
jPanel1 = new JPanel();
jScrollPane2 = new JScrollPane();
jTextArea1 = new JTextArea();
jPanel2 = new JPanel();
jButton1 = new JButton();
jSplitPane1.setDividerLocation(500);
jSplitPane1.setOrientation(JSplitPane.VERTICAL_SPLIT);
jEditorPane1.setContentType("text/html");
jPanel1.setLayout(new java.awt.BorderLayout());
jTextArea1.setColumns(20);
jTextArea1.setRows(5);
jTextArea1.addKeyListener(new java.awt.event.KeyAdapter() {
@Override
public void keyTyped(java.awt.event.KeyEvent evt) {
if (evt.getKeyChar() == '\n') {
sendMessage();
}
}
});
jButton1.setText("Send");
jScrollPane1.setViewportView(jEditorPane1);
jScrollPane2.setViewportView(jTextArea1);
jSplitPane1.setTopComponent(jScrollPane1);
jPanel1.add(jScrollPane2, java.awt.BorderLayout.CENTER);
jPanel2.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.RIGHT));
jButton1.addActionListener((java.awt.event.ActionEvent evt) -> {
sendMessage();
});
jPanel2.add(jButton1);
jPanel1.add(jPanel2, java.awt.BorderLayout.PAGE_END);
jSplitPane1.setRightComponent(jPanel1);
getContentPane().add(jSplitPane1, java.awt.BorderLayout.CENTER);
pack();
}
private void sendMessage() {
if (jTextArea1.getText().trim().isEmpty()) {
return;
}
ChatMessage message = new ChatMessage(connectionManager.getUserName(), jTextArea1.getText());
this.sendChatMessage(message);
jTextArea1.setText("");
}
public void sendChatMessage(Message message) {
connectionManager.sendUdpMulticast(message);
}
public void receiveChatMessage(ChatMessage message) {
SimpleDateFormat dt = new SimpleDateFormat("dd/MM/yyyyy hh:mm:ss");
Date date = new Date();
this.receiveChatMessage(dt.format(date) + " "
+ "<b>" + message.getName() + "</b>: "
+ message.getText() + "<br>");
}
public void receiveChatMessage(String message) {
jEditorPane1.setText(message + jEditorPane1.getText());
}
}
@@ -0,0 +1,96 @@
package pitiupi.chat.GUI;
import pitiupi.chat.net.LegacyMpChatMessage;
import pitiupi.control.ConnectionManager;
import pitiupi.net.tcp.TcpConnection;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import java.net.InetAddress;
import java.util.logging.Level;
import java.util.logging.Logger;
public class PrivateChatWindow extends JFrame {
private final ConnectionManager connectionManager;
private final TcpConnection connection;
private final JTextArea receivedMessagesArea;
private final JTextField messageField;
public PrivateChatWindow(ConnectionManager connectionManager, TcpConnection connection) {
this.connectionManager = connectionManager;
this.connection = connection;
setTitle("Chat privado (TCP)");
setSize(700, 500);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(10, 10, 10, 10));
receivedMessagesArea = new JTextArea();
receivedMessagesArea.setEditable(false);
receivedMessagesArea.setLineWrap(true);
receivedMessagesArea.setWrapStyleWord(true);
JScrollPane messagesScrollPane = new JScrollPane(receivedMessagesArea);
messagesScrollPane.setBorder(javax.swing.BorderFactory.createTitledBorder("Mensagens"));
mainPanel.add(messagesScrollPane, BorderLayout.CENTER);
messageField = new JTextField();
JButton tcpButton = new JButton("Enviar TCP");
tcpButton.addActionListener(e -> sendTCP());
JPanel bottomPanel = new JPanel(new BorderLayout(10, 10));
bottomPanel.add(messageField, BorderLayout.CENTER);
bottomPanel.add(tcpButton, BorderLayout.EAST);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
}
public void sendChatLine(String text) {
if (text == null || text.isBlank()) {
return;
}
messageField.setText(text);
sendTCP();
}
private void sendTCP() {
try {
LegacyMpChatMessage message = new LegacyMpChatMessage(
connectionManager.getUserName(),
pitiupi.presentation.shell.LocalNetworkInfo.bestLocalIpv4(),
"TCP privado",
messageField.getText()
);
connection.send(message.toByteArray());
receiveMessage(message);
messageField.setText("");
} catch (Exception ex) {
Logger.getLogger(PrivateChatWindow.class.getName()).log(Level.SEVERE, null, ex);
}
}
public void receiveMessage(LegacyMpChatMessage message) {
SwingUtilities.invokeLater(() -> {
String texto = String.format(
"[%s] %s (%s): %s%n",
message.getTipoMensagem(),
message.getNome(),
message.getIp(),
message.getMessage()
);
receivedMessagesArea.append(texto);
receivedMessagesArea.setCaretPosition(receivedMessagesArea.getDocument().getLength());
});
}
}
@@ -0,0 +1,109 @@
package pitiupi.chat.control;
import pitiupi.chat.net.LegacyMpChatMessage;
import pitiupi.control.ConnectionManager;
import pitiupi.models.User;
import pitiupi.net.tcp.TcpConnection;
import pitiupi.presentation.shell.LocalNetworkInfo;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class PrivateChatController {
private final ConnectionManager connectionManager;
private final PrivateChatHost host;
private final Map<String, TcpConnection> connections = new HashMap<>();
private final Set<String> pendingConnections = new HashSet<>();
private PrivateChatUiSink uiSink;
public PrivateChatController(ConnectionManager connectionManager, PrivateChatHost host) {
this.connectionManager = connectionManager;
this.host = host;
}
public void setUiSink(PrivateChatUiSink uiSink) {
this.uiSink = uiSink;
}
private static String addressKey(TcpConnection connection) {
return connection.getAddress().getHostAddress();
}
public void openChat(User user) {
if (user == null || user.getAddress() == null) {
return;
}
String address = user.getAddress().getHostAddress();
if (connections.containsKey(address)) {
if (uiSink != null) {
uiSink.onPrivateChannelReady(address);
}
return;
}
if (pendingConnections.contains(address)) {
return;
}
pendingConnections.add(address);
host.openConnection(address);
}
public void receiveMessage(TcpConnection connection, LegacyMpChatMessage message) {
if (uiSink != null) {
uiSink.appendPrivateLine(addressKey(connection), message, false);
}
}
public void connectionReceived(TcpConnection connection) {
String address = addressKey(connection);
pendingConnections.remove(address);
connections.put(address, connection);
if (uiSink != null) {
uiSink.onPrivateChannelReady(address);
}
}
public void closeChat(TcpConnection connection) {
String address = addressKey(connection);
connections.remove(address);
if (uiSink != null) {
uiSink.onPrivateChannelClosed(address);
}
}
public boolean sendText(User user, String text) {
if (user == null || user.getAddress() == null || text == null || text.isBlank()) {
return false;
}
String address = user.getAddress().getHostAddress();
TcpConnection connection = connections.get(address);
if (connection == null) {
return false;
}
try {
LegacyMpChatMessage message = new LegacyMpChatMessage(
connectionManager.getUserName(),
LocalNetworkInfo.bestLocalIpv4(),
"TCP privado",
text
);
connection.send(message.toByteArray());
if (uiSink != null) {
uiSink.appendPrivateLine(address, message, true);
}
return true;
} catch (Exception ex) {
System.out.println("Error sending private TCP: " + ex.getMessage());
return false;
}
}
public boolean hasOpenChannel(User user) {
if (user == null || user.getAddress() == null) {
return false;
}
return connections.containsKey(user.getAddress().getHostAddress());
}
}
@@ -0,0 +1,7 @@
package pitiupi.chat.control;
public interface PrivateChatHost {
String getPluginIdentifier();
void openConnection(String ip);
}
@@ -0,0 +1,15 @@
package pitiupi.chat.control;
import pitiupi.chat.net.LegacyMpChatMessage;
/**
* TCP privado integrado ao painel principal (sem janela extra).
*/
public interface PrivateChatUiSink {
void onPrivateChannelReady(String peerIp);
void onPrivateChannelClosed(String peerIp);
void appendPrivateLine(String peerIp, LegacyMpChatMessage message, boolean outgoing);
}
@@ -0,0 +1,58 @@
package pitiupi.chat.net;
import pitiupi.application.messaging.TransportMode;
import pitiupi.models.Messages.Message;
public class ChatMessage extends Message {
private String text;
private String name;
private String transportLabel;
private String senderIp;
public ChatMessage() {
}
public ChatMessage(String name, String text) {
this(name, text, TransportMode.MULTICAST_UDP.getLabel(), null);
}
public ChatMessage(String name, String text, String transportLabel, String senderIp) {
this.name = name;
this.text = text;
this.transportLabel = transportLabel;
this.senderIp = senderIp;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getTransportLabel() {
return transportLabel;
}
public void setTransportLabel(String transportLabel) {
this.transportLabel = transportLabel;
}
public String getSenderIp() {
return senderIp;
}
public void setSenderIp(String senderIp) {
this.senderIp = senderIp;
}
}
@@ -0,0 +1,39 @@
package pitiupi.chat.net;
import pitiupi.models.Messages.Message;
/**
* Formato legado do antigo plugin mpChat (TCP privado e salas antigas).
*/
public class LegacyMpChatMessage extends Message {
private String nome;
private String ip;
private String tipoMensagem;
private String message;
public LegacyMpChatMessage() {
}
public LegacyMpChatMessage(String nome, String ip, String tipoMensagem, String message) {
this.nome = nome;
this.ip = ip;
this.tipoMensagem = tipoMensagem;
this.message = message;
}
public String getNome() {
return nome;
}
public String getIp() {
return ip;
}
public String getTipoMensagem() {
return tipoMensagem;
}
public String getMessage() {
return message;
}
}
@@ -0,0 +1,235 @@
package pitiupi.chat.plugin;
import pitiupi.api.plugin.PeerActionsRegistrar;
import pitiupi.api.plugin.PluginWorkspace;
import pitiupi.application.activity.PeerActivityInbox;
import pitiupi.application.peeractions.PeerAction;
import pitiupi.application.peeractions.PeerActionTarget;
import pitiupi.application.peeractions.PeerTarget;
import pitiupi.application.presence.UserPeerView;
import pitiupi.chat.GUI.ChatPanel;
import pitiupi.chat.control.PrivateChatController;
import pitiupi.chat.control.PrivateChatHost;
import pitiupi.chat.net.ChatMessage;
import pitiupi.chat.net.LegacyMpChatMessage;
import pitiupi.control.ConnectionManager;
import pitiupi.presentation.shell.LocalUserIdentity;
import pitiupi.control.GuiManager;
import pitiupi.models.Messages.Message;
import pitiupi.models.Plugin;
import pitiupi.models.User;
import pitiupi.net.tcp.TcpConnection;
import javax.swing.SwingUtilities;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.InetAddress;
import java.util.List;
public class ChatPlugin extends Plugin implements PrivateChatHost {
private ChatPanel chatPanel;
private PluginWorkspace workspace;
private ConnectionManager connectionManager;
private PrivateChatController privateChatController;
@Override
public void createPlugin(ConnectionManager connectionManager, PluginWorkspace workspace) {
this.connectionManager = connectionManager;
this.workspace = workspace;
this.privateChatController = new PrivateChatController(connectionManager, this);
this.chatPanel = new ChatPanel(connectionManager);
chatPanel.setPrivateChatController(privateChatController);
privateChatController.setUiSink(chatPanel);
bindShellWorkspaceContent(chatPanel);
installPeerActions(workspace.peerActions());
}
@Override
public void createPlugin(ConnectionManager connectionManager, GuiManager guiManager) {
}
@Override
public void installPeerActions(PeerActionsRegistrar registrar) {
registrar.register(PeerAction.of(
"chat.focus",
"Abrir sala geral",
PeerActionTarget.ALL_PEERS,
target -> {
chatPanel.focusGeneralRoom();
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
}
));
registrar.register(PeerAction.of(
"chat.peer",
"Conversar com este peer",
PeerActionTarget.SINGLE_PEER,
this::openChatForPeer
));
registrar.register(PeerAction.of(
"chat.private.tcp",
"Ativar TCP privado",
PeerActionTarget.SINGLE_PEER,
target -> {
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
if (target instanceof PeerTarget.Single single) {
chatPanel.enablePrivateTcpForPeer(single.peer());
}
}
));
}
private void openChatForPeer(PeerTarget target) {
if (!(target instanceof PeerTarget.Single single)) {
return;
}
chatPanel.focusPeer(single.peer());
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
}
private static User toUser(UserPeerView peer) {
if (peer.isBroadcastTarget() || peer.isSelf()) {
return null;
}
return peer.getInetAddress()
.map(addr -> new User(peer.getDisplayName(), addr, System.currentTimeMillis(), peer.getInstanceId()))
.orElse(null);
}
@Override
public List<Class<? extends Message>> getMessages() {
return List.of(ChatMessage.class, LegacyMpChatMessage.class);
}
@Override
public void receiveMessage(Message message) {
if (message instanceof ChatMessage msg && chatPanel != null) {
recordChatActivity(msg);
SwingUtilities.invokeLater(() -> chatPanel.receiveChatMessage(msg));
} else if (message instanceof LegacyMpChatMessage legacy && chatPanel != null) {
SwingUtilities.invokeLater(() -> chatPanel.receiveLegacyMessage(
legacy.getTipoMensagem(),
legacy.getNome(),
legacy.getIp(),
legacy.getMessage()
));
}
}
private void recordChatActivity(ChatMessage msg) {
if (LocalUserIdentity.isLocalSender(connectionManager, msg.getName(), msg.getSenderIp())) {
return;
}
PeerActivityInbox inbox = connectionManager.getPeerActivityInbox();
String tag = msg.getTransportLabel();
boolean multicast = tag != null && tag.toLowerCase().contains("multicast");
if (multicast) {
inbox.recordBroadcast(getIdentifier(), "chat.focus");
return;
}
String instanceId = peerInstanceIdForActivity(msg.getSenderIp());
if (instanceId != null) {
inbox.record(instanceId, getIdentifier(), "chat.peer");
}
}
private String peerInstanceIdForActivity(String ip) {
if (ip == null || ip.isBlank() || connectionManager == null) {
return null;
}
try {
InetAddress address = InetAddress.getByName(ip);
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
if (view.isSelf()) {
continue;
}
if (view.getInetAddress().isPresent() && view.getInetAddress().get().equals(address)) {
return view.getInstanceId();
}
}
} catch (Exception ignored) {
}
return null;
}
@Override
public boolean receivePrivateConnection(TcpConnection connection) {
SwingUtilities.invokeLater(() -> privateChatController.connectionReceived(connection));
return true;
}
@Override
public boolean receiveBytes(TcpConnection connection, byte[] bytes) {
LegacyMpChatMessage legacy = parseLegacy(bytes);
if (legacy == null) {
return true;
}
privateChatController.receiveMessage(connection, legacy);
String instanceId = peerInstanceIdForActivity(connection.getAddress().getHostAddress());
if (instanceId != null) {
connectionManager.getPeerActivityInbox().record(instanceId, getIdentifier(), "chat.private.tcp");
}
return true;
}
@Override
public boolean onSocketClosed(TcpConnection connection) {
privateChatController.closeChat(connection);
return true;
}
@Override
public void openConnection(String ip) {
try {
connectionManager.connectPluginTcp(
InetAddress.getByName(ip),
getIdentifier(),
getIdentifier()
);
} catch (Exception e) {
System.out.println("Error opening connection: " + e.getMessage());
}
}
@Override
public String getPluginIdentifier() {
return getIdentifier();
}
private LegacyMpChatMessage parseLegacy(byte[] bytes) {
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
if (ois.readObject() instanceof LegacyMpChatMessage msg) {
return msg;
}
} catch (IOException | ClassNotFoundException ex) {
System.out.println(ex.toString());
}
return null;
}
@Override
public String getName() {
return "Chat";
}
@Override
public String getDisplayName() {
return "Conversa na rede";
}
@Override
public String getAuthor() {
return "Flávio Schiavoni";
}
@Override
public String getVersion() {
return "1";
}
@Override
public String getIdentifier() {
return "Pitiupi.Chat.Example";
}
}