Tabbed chats with BBCode rendering and clickable identities
Replace the chat target dropdown with a tab strip along the bottom of the chat panel: a Server and a Channel tab plus one closable tab per private chat, each with its own log and an unread marker. Render logs and channel/client descriptions as HTML through a new BBCode renderer in core, supporting b/i/u/s, color, size, url, center and lists, with auto-linked bare URLs. Input is escaped and tag values validated, so server-supplied text cannot inject markup or unsafe schemes. Sender names and TeamSpeak's own client:// links open the same context menu as the server tree (menus extracted into ClientMenu/ChannelMenu); channel:// links open the channel menu. Links are styled by CSS class: identities bold in the author colour, other TS protocols plain, and links leaving the client underlined so they cannot be mistaken for an identity. Dragging a client or channel out of the tree yields the TS3 link BBCode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import com.ts3client.net.ChannelNode;
|
||||
import com.ts3client.net.ClientEntry;
|
||||
import com.ts3client.net.ConnectionListener;
|
||||
import com.ts3client.net.TeamspeakConnection;
|
||||
import com.ts3client.text.TsLink;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
@@ -102,6 +103,17 @@ public final class MainFrame extends JFrame implements ConnectionListener, Serve
|
||||
this.treePanel = new ServerTreePanel(conn.getModel(), this);
|
||||
this.chatPanel = new ChatPanel();
|
||||
chatPanel.setSendHandler(this::onSendChat);
|
||||
chatPanel.setLinkHandler(new ChatPanel.LinkHandler() {
|
||||
@Override
|
||||
public void onClientLink(TsLink.Ref ref, Component source, int x, int y) {
|
||||
MainFrame.this.onClientLink(ref, source, x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChannelLink(TsLink.Ref ref, Component source, int x, int y) {
|
||||
MainFrame.this.onChannelLink(ref, source, x, y);
|
||||
}
|
||||
});
|
||||
|
||||
setJMenuBar(buildMenuBar());
|
||||
add(buildToolbar(), BorderLayout.NORTH);
|
||||
@@ -460,17 +472,59 @@ public final class MainFrame extends JFrame implements ConnectionListener, Serve
|
||||
"About TS3J", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
private void onSendChat(ChatPanel.Target target, String text) {
|
||||
private void onSendChat(ChatPanel.Target target, int clientId, String text) {
|
||||
if (!conn.isConnected()) {
|
||||
chatPanel.appendSystem("Not connected.");
|
||||
return;
|
||||
}
|
||||
if (target == ChatPanel.Target.SERVER) {
|
||||
conn.sendServerMessage(text);
|
||||
} else {
|
||||
conn.sendChannelMessage(text);
|
||||
String me = settings.nickname + " (you)";
|
||||
int myId = conn.getSelfClientId();
|
||||
switch (target) {
|
||||
case SERVER:
|
||||
conn.sendServerMessage(text);
|
||||
chatPanel.appendServerMessage(myId, me, text);
|
||||
break;
|
||||
case PRIVATE:
|
||||
conn.sendPrivateMessage(clientId, text);
|
||||
chatPanel.appendPrivateMessage(clientId, peerName(clientId), myId, me, text);
|
||||
break;
|
||||
default:
|
||||
conn.sendChannelMessage(text);
|
||||
chatPanel.appendChannelMessage(myId, me, text);
|
||||
break;
|
||||
}
|
||||
chatPanel.appendMessage(settings.nickname + " (you)", text);
|
||||
}
|
||||
|
||||
/** A client link in the chat log was clicked: show the same menu as the tree does. */
|
||||
private void onClientLink(TsLink.Ref ref, Component source, int x, int y) {
|
||||
ClientEntry client = conn.getModel().getClient(ref.id);
|
||||
// The id is only valid for the session the link was made in; fall back to
|
||||
// the unique id (and finally the nickname) so older links still resolve.
|
||||
if (client == null || (!ref.uniqueId.isEmpty() && !ref.uniqueId.equals(client.uniqueId))) {
|
||||
ClientEntry byUid = ref.uniqueId.isEmpty() ? null
|
||||
: conn.getModel().findClientByUniqueId(ref.uniqueId);
|
||||
if (byUid == null && !ref.name.isEmpty()) byUid = conn.getModel().findClientByName(ref.name);
|
||||
if (byUid != null) client = byUid;
|
||||
}
|
||||
if (client == null) {
|
||||
chatPanel.appendSystem("That client is no longer on the server.");
|
||||
return;
|
||||
}
|
||||
ClientMenu.build(client, client.id == conn.getSelfClientId(), this).show(source, x, y);
|
||||
}
|
||||
|
||||
private void onChannelLink(TsLink.Ref ref, Component source, int x, int y) {
|
||||
ChannelNode channel = conn.getModel().getChannel(ref.id);
|
||||
if (channel == null) {
|
||||
chatPanel.appendSystem("That channel no longer exists.");
|
||||
return;
|
||||
}
|
||||
ChannelMenu.build(channel, this).show(source, x, y);
|
||||
}
|
||||
|
||||
private String peerName(int clientId) {
|
||||
ClientEntry c = conn.getModel().getClient(clientId);
|
||||
return c != null ? c.nickname : "Client " + clientId;
|
||||
}
|
||||
|
||||
private void updateButtons(boolean connected) {
|
||||
@@ -503,11 +557,7 @@ public final class MainFrame extends JFrame implements ConnectionListener, Serve
|
||||
|
||||
@Override
|
||||
public void openPrivateChat(ClientEntry client) {
|
||||
String msg = JOptionPane.showInputDialog(this, "Message to " + client.nickname + ":");
|
||||
if (msg != null && !msg.trim().isEmpty()) {
|
||||
conn.sendPrivateMessage(client.id, msg.trim());
|
||||
chatPanel.appendMessage("You → " + client.nickname, msg.trim());
|
||||
}
|
||||
chatPanel.openPrivateChat(client.id, client.nickname);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -597,6 +647,7 @@ public final class MainFrame extends JFrame implements ConnectionListener, Serve
|
||||
treePanel.showDisconnected();
|
||||
currentSelection = null;
|
||||
infoPanel.clear();
|
||||
chatPanel.closePrivateChats();
|
||||
chatPanel.appendSystem("Disconnected" + (reason == null || reason.isEmpty() ? "." : ": " + reason));
|
||||
});
|
||||
}
|
||||
@@ -616,8 +667,17 @@ public final class MainFrame extends JFrame implements ConnectionListener, Serve
|
||||
|
||||
@Override
|
||||
public void onChat(ChatScope scope, int fromClientId, String fromName, String message) {
|
||||
String prefix = scope == ChatScope.PRIVATE ? "[PM] " : scope == ChatScope.SERVER ? "[Server] " : "";
|
||||
chatPanel.appendMessage(prefix + fromName, message);
|
||||
switch (scope) {
|
||||
case PRIVATE:
|
||||
chatPanel.appendPrivateMessage(fromClientId, fromName, fromClientId, fromName, message);
|
||||
break;
|
||||
case SERVER:
|
||||
chatPanel.appendServerMessage(fromClientId, fromName, message);
|
||||
break;
|
||||
default:
|
||||
chatPanel.appendChannelMessage(fromClientId, fromName, message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user