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

+5
View File
@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
+9
View File
@@ -0,0 +1,9 @@
<component name="libraryTable">
<library name="pitiupi.chat">
<CLASSES>
<root url="jar://$PROJECT_DIR$/dist/pitiupi.chat.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>
+5
View File
@@ -0,0 +1,5 @@
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="openjdk-26" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/pitiupi.chat.iml" filepath="$PROJECT_DIR$/pitiupi.chat.iml" />
</modules>
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
+73
View File
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- You may freely edit this file. See commented blocks below for -->
<!-- some examples of how to customize the build. -->
<!-- (If you delete it and reopen the project it will be recreated.) -->
<!-- By default, only the Clean and Build commands use this build script. -->
<!-- Commands such as Run, Debug, and Test only use this build script if -->
<!-- the Compile on Save feature is turned off for the project. -->
<!-- You can turn off the Compile on Save (or Deploy on Save) setting -->
<!-- in the project's Project Properties dialog box.-->
<project name="pitiupi.chat" default="default" basedir=".">
<description>Builds, tests, and runs the project pitiupi.chat.</description>
<import file="nbproject/build-impl.xml"/>
<!--
There exist several targets which are by default empty and which can be
used for execution of your tasks. These targets are usually executed
before and after some main targets. They are:
-pre-init: called before initialization of project properties
-post-init: called after initialization of project properties
-pre-compile: called before javac compilation
-post-compile: called after javac compilation
-pre-compile-single: called before javac compilation of single file
-post-compile-single: called after javac compilation of single file
-pre-compile-test: called before javac compilation of JUnit tests
-post-compile-test: called after javac compilation of JUnit tests
-pre-compile-test-single: called before javac compilation of single JUnit test
-post-compile-test-single: called after javac compilation of single JUunit test
-pre-jar: called before JAR building
-post-jar: called after JAR building
-post-clean: called after cleaning build products
(Targets beginning with '-' are not intended to be called on their own.)
Example of inserting an obfuscator after compilation could look like this:
<target name="-post-compile">
<obfuscate>
<fileset dir="${build.classes.dir}"/>
</obfuscate>
</target>
For list of available properties check the imported
nbproject/build-impl.xml file.
Another way to customize the build is by overriding existing main targets.
The targets of interest are:
-init-macrodef-javac: defines macro for javac compilation
-init-macrodef-junit: defines macro for junit execution
-init-macrodef-debug: defines macro for class debugging
-init-macrodef-java: defines macro for class execution
-do-jar: JAR building
run: execution of project
-javadoc-build: Javadoc generation
test-report: JUnit report generation
An example of overriding the target for project execution could look like this:
<target name="run" depends="pitiupi.chat-impl.jar">
<exec dir="bin" executable="launcher.exe">
<arg file="${dist.jar}"/>
</exec>
</target>
Notice that the overridden target depends on the jar target and not only on
the compile target as the regular run target does. Again, for a list of available
properties which you can use, check the target you are overriding in the
nbproject/build-impl.xml file.
-->
</project>
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,8 @@
build.xml.data.CRC32=3f66efc2
build.xml.script.CRC32=3953f1a4
build.xml.stylesheet.CRC32=f85dc8f2@1.119.0.48
# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
nbproject/build-impl.xml.data.CRC32=3f66efc2
nbproject/build-impl.xml.script.CRC32=a4594806
nbproject/build-impl.xml.stylesheet.CRC32=12e0a6c2@1.119.0.48
+98
View File
@@ -0,0 +1,98 @@
annotation.processing.enabled=true
annotation.processing.enabled.in.editor=false
annotation.processing.processors.list=
annotation.processing.run.all.processors=true
annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output
application.title=pitiupi.chat
application.vendor=flavio
build.classes.dir=${build.dir}/classes
build.classes.excludes=**/*.java,**/*.form
# This directory is removed when the project is cleaned:
build.dir=build
build.generated.dir=${build.dir}/generated
build.generated.sources.dir=${build.dir}/generated-sources
# Only compile against the classpath explicitly listed here:
build.sysclasspath=ignore
build.test.classes.dir=${build.dir}/test/classes
build.test.results.dir=${build.dir}/test/results
# Uncomment to specify the preferred debugger connection transport:
#debug.transport=dt_socket
debug.classpath=\
${run.classpath}
debug.modulepath=\
${run.modulepath}
debug.test.classpath=\
${run.test.classpath}
debug.test.modulepath=\
${run.test.modulepath}
# Files in build.classes.dir which should be excluded from distribution jar
dist.archive.excludes=
# This directory is removed when the project is cleaned:
dist.dir=dist
dist.jar=${dist.dir}/pitiupi.chat.jar
dist.javadoc.dir=${dist.dir}/javadoc
dist.jlink.dir=${dist.dir}/jlink
dist.jlink.output=${dist.jlink.dir}/pitiupi.chat
endorsed.classpath=
excludes=
includes=**
jar.compress=false
javac.classpath=\
${reference.pitiupi.jar}
# Space-separated list of extra javac options
javac.compilerargs=
javac.deprecation=false
javac.external.vm=true
javac.modulepath=
javac.processormodulepath=
javac.processorpath=\
${javac.classpath}
javac.source=25
javac.target=25
javac.test.classpath=\
${javac.classpath}:\
${build.classes.dir}
javac.test.modulepath=\
${javac.modulepath}
javac.test.processorpath=\
${javac.test.classpath}
javadoc.additionalparam=
javadoc.author=false
javadoc.encoding=${source.encoding}
javadoc.html5=false
javadoc.noindex=false
javadoc.nonavbar=false
javadoc.notree=false
javadoc.private=false
javadoc.splitindex=true
javadoc.use=true
javadoc.version=false
javadoc.windowtitle=
# The jlink additional root modules to resolve
jlink.additionalmodules=
# The jlink additional command line parameters
jlink.additionalparam=
jlink.launcher=true
jlink.launcher.name=pitiupi.chat
meta.inf.dir=${src.dir}/META-INF
mkdist.disabled=true
platform.active=default_platform
project.pitiupi=../../pitiupi
reference.pitiupi.jar=${project.pitiupi}/dist/pitiupi.jar
run.classpath=\
${javac.classpath}:\
${build.classes.dir}
# Space-separated list of JVM arguments used when running the project.
# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value.
# To set system properties for unit tests define test-sys-prop.name=value:
run.jvmargs=
run.modulepath=\
${javac.modulepath}
run.test.classpath=\
${javac.test.classpath}:\
${build.test.classes.dir}
run.test.modulepath=\
${javac.test.modulepath}
source.encoding=UTF-8
src.dir=src
test.src.dir=test
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://www.netbeans.org/ns/project/1">
<type>org.netbeans.modules.java.j2seproject</type>
<configuration>
<data xmlns="http://www.netbeans.org/ns/j2se-project/3">
<name>pitiupi.chat</name>
<source-roots>
<root id="src.dir"/>
</source-roots>
<test-roots>
<root id="test.src.dir"/>
</test-roots>
</data>
<references xmlns="http://www.netbeans.org/ns/ant-project-references/1">
<reference>
<foreign-project>pitiupi</foreign-project>
<artifact-type>jar</artifact-type>
<script>build.xml</script>
<target>jar</target>
<clean-target>clean</clean-target>
<id>jar</id>
</reference>
</references>
</configuration>
</project>
+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$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../../pitiupi/dist/pitiupi.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
</component>
</module>
@@ -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";
}
}