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:
2026-08-13 13:50:04 +00:00
parent a69b7bde74
commit 707d9ebbba
11 changed files with 800 additions and 103 deletions

View File

@@ -0,0 +1,42 @@
package com.ts3client.ui;
import com.ts3client.net.ClientEntry;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
/**
* The context menu for a client, shared by the server tree and the clickable
* names in the chat log.
*/
final class ClientMenu {
private ClientMenu() {
}
static JPopupMenu build(ClientEntry client, boolean self, ServerTreePanel.Actions actions) {
JPopupMenu menu = new JPopupMenu();
if (!self) {
JMenuItem pm = new JMenuItem("Open text chat");
pm.addActionListener(a -> actions.openPrivateChat(client));
menu.add(pm);
JMenuItem poke = new JMenuItem("Poke");
poke.addActionListener(a -> actions.pokeClient(client));
menu.add(poke);
menu.addSeparator();
boolean muted = actions.isClientLocallyMuted(client.id);
JMenuItem mute = new JMenuItem(muted ? "Unmute client" : "Mute client");
mute.addActionListener(a -> actions.toggleClientMute(client));
menu.add(mute);
} else {
JMenuItem me = new JMenuItem("This is you");
me.setEnabled(false);
menu.add(me);
}
menu.addSeparator();
JMenuItem info = new JMenuItem("Connection Info");
info.addActionListener(a -> actions.showConnectionInfo(client));
menu.add(info);
return menu;
}
}