feat(screencast,chat): roteamento read-only, avisos de sessão e TCP privado

SessionNoticeMessage encerra abas de recebimento; abas ↑/↓; chat unifica bucket peer e conexão TCP.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Gabriel Felipe Souza SantosandCursor committed 2026-10-27 18:48:16 -03:00
1 parent fc657345ba
commit 112b6edaeb
14 files changed
+406 -36

No files matched your search

@@ -305,8 +305,8 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
@Override
public void onPrivateChannelReady(String peerIp) {
SwingUtilities.invokeLater(() -> {
appendSystemLine(privateKey(peerIp), "Canal TCP privado conectado com " + peerIp + ".");
if (privateKey(peerIp).equals(activeConversationKey)) {
appendSystemLine(peerKey(canonicalPeerIp(peerIp)), "Canal TCP privado conectado com " + peerIp + ".");
if (peerKey(canonicalPeerIp(peerIp)).equals(activeConversationKey)) {
flushTranscriptToPane();
}
});
@@ -315,8 +315,8 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
@Override
public void onPrivateChannelClosed(String peerIp) {
SwingUtilities.invokeLater(() -> {
appendSystemLine(privateKey(peerIp), "Canal TCP privado encerrado (" + peerIp + ").");
if (privateKey(peerIp).equals(activeConversationKey)) {
appendSystemLine(peerKey(canonicalPeerIp(peerIp)), "Canal TCP privado encerrado (" + peerIp + ").");
if (peerKey(canonicalPeerIp(peerIp)).equals(activeConversationKey)) {
flushTranscriptToPane();
}
});
@@ -324,7 +324,13 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
@Override
public void appendPrivateLine(String peerIp, LegacyMpChatMessage message, boolean outgoing) {
SwingUtilities.invokeLater(() -> appendLegacyLine(privateKey(peerIp), message));
SwingUtilities.invokeLater(() -> {
String canonical = canonicalPeerIp(peerIp);
if (!outgoing) {
ensurePeerTabForIncoming(canonical, message.getNome());
}
appendLegacyLine(peerKey(canonical), message);
});
}
private void buildUi() {
@@ -418,8 +424,8 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
if (privateChatController != null) {
privateChatController.openChat(user);
}
appendSystemLine(privateKey(PeerIpv4.fromInetAddress(user.getAddress())),
"Canal TCP privado conectando… envie de novo em instantes.");
appendSystemLine(peerKey(canonicalPeerIp(PeerIpv4.fromInetAddress(user.getAddress()))),
"Canal TCP privado conectando…");
return;
}
@@ -499,7 +505,7 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
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;
String bucket = ip != null && !ip.isBlank() ? peerKey(canonicalPeerIp(ip)) : ROOM_KEY;
appendLegacyLine(bucket, legacy);
});
}
@@ -551,7 +557,7 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
}
if (key != null && key.startsWith("peer:") && !PluginSessionKeys.PEER_DRAFT.equals(key)) {
String tabIp = canonicalPeerIp(key.substring("peer:".length()));
return peerKey(tabIp).equals(bucket);
return peerKey(tabIp).equals(bucket) || privateKey(tabIp).equals(bucket);
}
return false;
}
@@ -609,9 +615,6 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
if (ip == null || ip.isEmpty()) {
return ROOM_KEY;
}
if (routing.privateTcpOn()) {
return privateKey(ip);
}
return peerKey(ip);
}
@@ -18,6 +18,7 @@ public class PrivateChatController {
private final PrivateChatHost host;
private final Map<String, TcpConnection> connections = new HashMap<>();
private final Set<String> pendingConnections = new HashSet<>();
private final Map<String, PendingSend> pendingTextByAddress = new HashMap<>();
private PrivateChatUiSink uiSink;
public PrivateChatController(ConnectionManager connectionManager, PrivateChatHost host) {
@@ -38,10 +39,9 @@ public class PrivateChatController {
return;
}
String address = PeerIpv4.fromInetAddress(user.getAddress());
if (connections.containsKey(address)) {
if (uiSink != null) {
uiSink.onPrivateChannelReady(address);
}
TcpConnection existing = connections.get(address);
if (existing != null && !existing.isClosed()) {
flushPendingSend(address, user);
return;
}
if (pendingConnections.contains(address)) {
@@ -51,6 +51,17 @@ public class PrivateChatController {
host.openConnection(address);
}
public void registerOutgoingConnection(String address, TcpConnection connection) {
if (address == null || connection == null) {
return;
}
String canonical = PeerIpv4.normalize(address);
pendingConnections.remove(canonical);
replaceConnection(canonical, connection, false);
User user = new User("", connection.getAddress(), System.currentTimeMillis(), null);
flushPendingSend(canonical, user);
}
public void receiveMessage(TcpConnection connection, LegacyMpChatMessage message) {
if (uiSink != null) {
uiSink.appendPrivateLine(addressKey(connection), message, false);
@@ -60,8 +71,17 @@ public class PrivateChatController {
public void connectionReceived(TcpConnection connection) {
String address = addressKey(connection);
pendingConnections.remove(address);
replaceConnection(address, connection, true);
}
private void replaceConnection(String address, TcpConnection connection, boolean notifyReady) {
TcpConnection existing = connections.get(address);
if (existing != null && !existing.isClosed() && existing != connection) {
return;
}
boolean first = existing == null || existing.isClosed();
connections.put(address, connection);
if (uiSink != null) {
if (notifyReady && first && uiSink != null) {
uiSink.onPrivateChannelReady(address);
}
}
@@ -69,6 +89,8 @@ public class PrivateChatController {
public void closeChat(TcpConnection connection) {
String address = addressKey(connection);
connections.remove(address);
pendingConnections.remove(address);
pendingTextByAddress.remove(address);
if (uiSink != null) {
uiSink.onPrivateChannelClosed(address);
}
@@ -80,9 +102,24 @@ public class PrivateChatController {
}
String address = PeerIpv4.fromInetAddress(user.getAddress());
TcpConnection connection = connections.get(address);
if (connection == null) {
if (connection == null || connection.isClosed()) {
if (pendingConnections.contains(address)) {
pendingTextByAddress.put(address, new PendingSend(user, text));
return true;
}
return false;
}
return sendOnConnection(connection, address, text);
}
private void flushPendingSend(String address, User user) {
PendingSend pending = pendingTextByAddress.remove(address);
if (pending != null) {
sendText(user != null ? user : pending.user, pending.text);
}
}
private boolean sendOnConnection(TcpConnection connection, String address, String text) {
try {
LegacyMpChatMessage message = new LegacyMpChatMessage(
connectionManager.getUserName(),
@@ -105,6 +142,10 @@ public class PrivateChatController {
if (user == null || user.getAddress() == null) {
return false;
}
return connections.containsKey(PeerIpv4.fromInetAddress(user.getAddress()));
TcpConnection connection = connections.get(PeerIpv4.fromInetAddress(user.getAddress()));
return connection != null && !connection.isClosed();
}
private record PendingSend(User user, String text) {
}
}
@@ -139,11 +139,12 @@ public class ChatPlugin extends Plugin implements PrivateChatHost {
@Override
public void openConnection(String ip) {
try {
connectionManager.connectPluginTcp(
TcpConnection connection = connectionManager.connectPluginTcp(
InetAddress.getByName(ip),
getIdentifier(),
getIdentifier()
);
privateChatController.registerOutgoingConnection(ip, connection);
} catch (Exception e) {
System.out.println("Error opening connection: " + e.getMessage());
}
@@ -7,6 +7,11 @@ import pitiupi.application.messaging.TransportMode;
import pitiupi.api.plugin.PeerActivityFocusTokens;
import pitiupi.application.presence.UserPeerView;
import pitiupi.control.ConnectionManager;
import pitiupi.application.session.SessionNoticeChannel;
import pitiupi.application.session.SessionNoticePhase;
import pitiupi.application.session.SessionNoticeScope;
import pitiupi.models.Messages.SessionNoticeMessage;
import pitiupi.presentation.shell.LocalNetworkInfo;
import pitiupi.presentation.shell.PeerDraftPanel;
import pitiupi.presentation.shell.PeerIpv4;
import pitiupi.presentation.shell.PluginRoutingPanel;
@@ -14,6 +19,7 @@ import pitiupi.presentation.shell.PluginSessionKeys;
import pitiupi.presentation.shell.PluginSessionWorkspace;
import pitiupi.presentation.shell.SessionPeerLabels;
import pitiupi.presentation.shell.SessionTabBar;
import pitiupi.presentation.shell.SessionTabDirectionLabels;
import pitiupi.presentation.shell.ui.ShellUi;
import pitiupi.screenCast.control.AlarmClock;
@@ -74,6 +80,8 @@ public class ScreenCastPanel extends JPanel {
private final Map<String, String> peerIpsByViewKey = new HashMap<>();
private final Map<String, String> peerDisplayNames = new HashMap<>();
private final Map<String, ScreenCastReceiveScope> receiveScopes = new HashMap<>();
private final Map<String, TransportMode> receiveTransportByViewKey = new HashMap<>();
private PluginRoutingPanel.RoutingSnapshot lastShareRoutingSnapshot;
private final CardLayout bodyLayout = new CardLayout();
private final JPanel bodyCards = new JPanel(bodyLayout);
private final Map<String, JPanel> bodyPanels = new HashMap<>();
@@ -105,7 +113,11 @@ public class ScreenCastPanel extends JPanel {
if (!ScreenCastOutboundTabs.isOutboundShareTabKey(key)) {
return;
}
sharingByTabKey.put(key, shareToggle.isSelected());
boolean nowOn = shareToggle.isSelected();
if (!nowOn) {
sendShareEndedNoticeForActiveTab();
}
sharingByTabKey.put(key, nowOn);
updateShareToggleLabel();
applySharingForActiveTab();
});
@@ -136,7 +148,7 @@ public class ScreenCastPanel extends JPanel {
private void initSessionTabs() {
sessionTabs.addOrSelectTab(
PluginSessionKeys.ROOM,
"Todos",
SessionTabDirectionLabels.outboundTitle("Todos"),
"Transmissão para todos na rede (multicast)",
new JPanel(),
false
@@ -164,10 +176,15 @@ public class ScreenCastPanel extends JPanel {
if (key != null && key.startsWith("view:")) {
peerIpsByViewKey.remove(key);
receiveScopes.remove(key);
receiveTransportByViewKey.remove(key);
bodyPanels.remove(key);
return;
}
boolean wasSharing = isSharingForTab(key);
if (wasSharing) {
applyRoutingForSessionKey(key);
sendShareEndedNotice(routingPanel.snapshot());
}
sharingByTabKey.remove(key);
if (wasSharing && key != null && key.equals(shareContextTabKey)) {
cancelShareTimer();
@@ -304,6 +321,7 @@ public class ScreenCastPanel extends JPanel {
return;
}
syncRoutingFromActiveSessionTab();
lastShareRoutingSnapshot = routingPanel.snapshot();
timer = new Timer();
alarmClock = new AlarmClock(this);
timer.scheduleAtFixedRate(alarmClock, 0, TIME);
@@ -356,14 +374,25 @@ public class ScreenCastPanel extends JPanel {
String ip = peerIpsByViewKey.get(key);
String title = key.substring("view:".length());
ScreenCastReceiveScope scope = receiveScopes.getOrDefault(key, ScreenCastReceiveScope.BROADCAST);
if (ip != null && !ip.isBlank()) {
routingPanel.bindPeerSession(ip, title, scope.bannerHtml());
} else {
routingPanel.bindPeerSession("", title, scope.bannerHtml());
}
TransportMode mode = receiveTransportByViewKey.getOrDefault(
key,
transportForReceiveScope(scope)
);
routingPanel.bindReceiveSession(
ip != null ? ip : "",
title,
scope.bannerHtml(),
mode
);
}
}
private static TransportMode transportForReceiveScope(ScreenCastReceiveScope scope) {
return scope == ScreenCastReceiveScope.DIRECT
? TransportMode.UNICAST_UDP
: TransportMode.MULTICAST_UDP;
}
private void openDraftPeerTab() {
String previous = sessionTabs.selectedKey();
if (previous != null && !PluginSessionKeys.PEER_DRAFT.equals(previous)) {
@@ -401,11 +430,12 @@ public class ScreenCastPanel extends JPanel {
peerDisplayNames.put(canonical, resolved);
ensureBodyPanel(key);
setBodyMessage(key, ShellUi.mutedLabel(IDLE_PEER_HTML));
String tabTitle = SessionTabDirectionLabels.outboundTitle(resolved);
if (sessionTabs.hasTab(key)) {
sessionTabs.setTabTitle(key, resolved, canonical);
sessionTabs.setTabTitle(key, tabTitle, canonical);
sessionTabs.selectTab(key);
} else {
sessionTabs.addOrSelectTab(key, resolved, canonical, new JPanel(), true);
sessionTabs.addOrSelectTab(key, tabTitle, canonical, new JPanel(), true);
}
routingPanel.bindPeerSession(canonical, resolved);
showBodyCard(key);
@@ -440,7 +470,7 @@ public class ScreenCastPanel extends JPanel {
if (!sessionTabs.hasTab(PluginSessionKeys.ROOM)) {
sessionTabs.addOrSelectTab(
PluginSessionKeys.ROOM,
"Todos",
SessionTabDirectionLabels.outboundTitle("Todos"),
"Transmissão para todos na rede (multicast)",
new JPanel(),
false
@@ -464,6 +494,9 @@ public class ScreenCastPanel extends JPanel {
}
public void stopSharing() {
if (isSharingForTab(sessionTabs.selectedKey())) {
sendShareEndedNoticeForActiveTab();
}
sharingByTabKey.replaceAll((k, v) -> false);
setShareToggleSelectedSilently(false);
cancelShareTimer();
@@ -560,9 +593,12 @@ public class ScreenCastPanel extends JPanel {
updateTabError(viewKey, message.getName(), "Não foi possível decodificar a imagem.", scope);
return;
}
receiveScopes.put(viewKey, scope != null ? scope : ScreenCastReceiveScope.BROADCAST);
ScreenCastReceiveScope resolvedScope = scope != null ? scope : ScreenCastReceiveScope.BROADCAST;
receiveScopes.put(viewKey, resolvedScope);
receiveTransportByViewKey.put(viewKey, transportForReceiveScope(resolvedScope));
resolveSenderIp(message.getName(), viewKey);
sessionTabs.addTabIfAbsent(viewKey, message.getName(), message.getName(), new JPanel(), true);
String tabTitle = SessionTabDirectionLabels.inboundTitle(message.getName());
sessionTabs.addTabIfAbsent(viewKey, tabTitle, message.getName(), new JPanel(), true);
setBodyContent(viewKey, new JScrollPane(new JLabel(new ImageIcon(image))));
if (viewKey.equals(sessionTabs.selectedKey())) {
showBodyCard(viewKey);
@@ -586,15 +622,83 @@ public class ScreenCastPanel extends JPanel {
}
private void updateTabError(String tabKey, String title, String error, ScreenCastReceiveScope scope) {
receiveScopes.put(tabKey, scope != null ? scope : ScreenCastReceiveScope.BROADCAST);
ScreenCastReceiveScope resolvedScope = scope != null ? scope : ScreenCastReceiveScope.BROADCAST;
receiveScopes.put(tabKey, resolvedScope);
receiveTransportByViewKey.put(tabKey, transportForReceiveScope(resolvedScope));
JLabel label = new JLabel("<html><p style='color:#c62828'>" + error + "</p></html>");
setBodyContent(tabKey, label);
sessionTabs.addTabIfAbsent(tabKey, title, title, new JPanel(), true);
String tabTitle = SessionTabDirectionLabels.inboundTitle(title);
sessionTabs.addTabIfAbsent(tabKey, tabTitle, title, new JPanel(), true);
if (tabKey.equals(sessionTabs.selectedKey())) {
showBodyCard(tabKey);
}
}
public void closeReceiveTab(String senderDisplayName) {
if (senderDisplayName == null || senderDisplayName.isBlank()) {
return;
}
String viewKey = "view:" + senderDisplayName;
if (!sessionTabs.hasTab(viewKey)) {
return;
}
SwingUtilities.invokeLater(() -> {
if (viewKey.equals(sessionTabs.selectedKey())) {
String fallback = sessionTabs.hasTab(PluginSessionKeys.ROOM)
? PluginSessionKeys.ROOM
: lastSessionKey;
if (fallback != null && sessionTabs.hasTab(fallback) && !fallback.startsWith("view:")) {
sessionTabs.selectTabSilently(fallback);
onSessionTabSelected(fallback);
}
}
peerIpsByViewKey.remove(viewKey);
receiveScopes.remove(viewKey);
receiveTransportByViewKey.remove(viewKey);
bodyPanels.remove(viewKey);
sessionTabs.closeTab(viewKey);
});
}
private void sendShareEndedNoticeForActiveTab() {
PluginRoutingPanel.RoutingSnapshot snap = lastShareRoutingSnapshot != null
? lastShareRoutingSnapshot
: routingPanel.snapshot();
sendShareEndedNotice(snap);
}
private void sendShareEndedNotice(PluginRoutingPanel.RoutingSnapshot routing) {
if (pluginId == null || routing == null) {
return;
}
TransportMode mode = routing.transport();
if (mode == null) {
return;
}
try {
SessionNoticeScope scope = routing.everyoneInRoom()
? SessionNoticeScope.BROADCAST
: SessionNoticeScope.DIRECT;
String localIp = PeerIpv4.normalize(LocalNetworkInfo.bestLocalIpv4());
SessionNoticeMessage notice = new SessionNoticeMessage(
pluginId,
SessionNoticePhase.END,
SessionNoticeChannel.SCREEN_CAST,
connectionManager.getUserName(),
localIp,
scope,
mode
);
SendTarget target = resolveSendTarget(routing);
if (target != null) {
MessageDispatcher.send(connectionManager, notice, target, mode);
}
} catch (Exception ex) {
Logger.getLogger(ScreenCastPanel.class.getName()).log(Level.WARNING, ex.getMessage());
}
lastShareRoutingSnapshot = null;
}
private void ensureBodyPanel(String key) {
if (!bodyPanels.containsKey(key)) {
JPanel panel = new JPanel(new BorderLayout());
@@ -11,7 +11,10 @@ import pitiupi.application.peeractions.PeerTarget;
import pitiupi.control.ConnectionManager;
import pitiupi.control.GuiManager;
import pitiupi.models.Menus.MenuCategory;
import pitiupi.application.session.SessionNoticeChannel;
import pitiupi.application.session.SessionNoticePhase;
import pitiupi.models.Messages.Message;
import pitiupi.models.Messages.SessionNoticeMessage;
import pitiupi.models.Plugin;
import pitiupi.screenCast.GUI.ScreenCastMenu;
import pitiupi.screenCast.GUI.ScreenCastPanel;
@@ -98,11 +101,15 @@ public class ScreenCastPlugin extends Plugin {
@Override
public List<Class<? extends Message>> getMessages() {
return List.of(ScreenCastMessage.class);
return List.of(ScreenCastMessage.class, SessionNoticeMessage.class);
}
@Override
public void receiveMessage(Message message) {
if (message instanceof SessionNoticeMessage notice && screenCastPanel != null) {
handleSessionNotice(notice);
return;
}
if (message instanceof ScreenCastMessage msg && screenCastPanel != null) {
if (isOwnOutboundFrame(msg)) {
return;
@@ -134,4 +141,25 @@ public class ScreenCastPlugin extends Plugin {
return ScreenCastReceiveScope.BROADCAST;
}
private void handleSessionNotice(SessionNoticeMessage notice) {
if (notice.getChannel() != SessionNoticeChannel.SCREEN_CAST) {
return;
}
if (notice.getPhase() != SessionNoticePhase.END) {
return;
}
if (!getIdentifier().equals(notice.getPluginId())) {
return;
}
String sender = notice.getSenderDisplayName();
if (sender == null) {
return;
}
String localName = connectionManager != null ? connectionManager.getUserName() : null;
if (localName != null && localName.equals(sender.trim())) {
return;
}
screenCastPanel.closeReceiveTab(sender);
}
}