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:
1 parent
b10c196fc9
commit
eeb1936f6d
178 files changed
+7641
-1481
No files matched your search
@@ -0,0 +1 @@
|
||||
pitiupi.chat.plugin.ChatPlugin
|
||||
@@ -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("&", "&").replace("<", "<").replace(">", ">");
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user