Update connection and chat handling
This commit is contained in:
@@ -586,8 +586,9 @@ final class ConnectionEventHandler implements TS3Listener {
|
|||||||
notification = SoundEvent.CHAT_RECEIVED_MESSAGE_SERVER;
|
notification = SoundEvent.CHAT_RECEIVED_MESSAGE_SERVER;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
conn.persistIncomingChat(scope, e.getInvokerId(), e.getInvokerUniqueId(), e.getInvokerName(), e.getMessage());
|
||||||
conn.sound(notification, conn.clientVars(e.getInvokerId(), e.getInvokerName()));
|
conn.sound(notification, conn.clientVars(e.getInvokerId(), e.getInvokerName()));
|
||||||
conn.ui.onChat(scope, e.getInvokerId(), e.getInvokerName(), e.getMessage());
|
conn.ui.onChat(scope, e.getInvokerId(), e.getInvokerUniqueId(), e.getInvokerName(), e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.ts3client.net;
|
package com.ts3client.net;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI-facing callbacks fired by {@link TeamspeakConnection}. Implementations are
|
* UI-facing callbacks fired by {@link TeamspeakConnection}. Implementations are
|
||||||
* responsible for marshalling to the Swing EDT.
|
* responsible for marshalling to the Swing EDT.
|
||||||
@@ -24,7 +26,15 @@ public interface ConnectionListener {
|
|||||||
/** A group icon finished downloading; repaint anything showing icons. */
|
/** A group icon finished downloading; repaint anything showing icons. */
|
||||||
void onIconsUpdated();
|
void onIconsUpdated();
|
||||||
|
|
||||||
void onChat(ChatScope scope, int fromClientId, String fromName, String message);
|
void onChat(ChatScope scope, int fromClientId, String fromUniqueId, String fromName, String message);
|
||||||
|
|
||||||
|
/** Previously persisted TS3 HTML log entries for one chat scope. */
|
||||||
|
default void onChatHistory(ChatScope scope, List<String> htmlEntries) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Previously persisted TS3 HTML log entries for a private chat. */
|
||||||
|
default void onPrivateChatHistory(int clientId, String nickname, List<String> htmlEntries) {
|
||||||
|
}
|
||||||
|
|
||||||
/** A client (possibly the local one) started or stopped talking. */
|
/** A client (possibly the local one) started or stopped talking. */
|
||||||
void onTalkStateChanged(int clientId, boolean talking);
|
void onTalkStateChanged(int clientId, boolean talking);
|
||||||
@@ -40,4 +50,9 @@ public interface ConnectionListener {
|
|||||||
* a group being (un)assigned, and so on.
|
* a group being (un)assigned, and so on.
|
||||||
*/
|
*/
|
||||||
void onServerLog(String message);
|
void onServerLog(String message);
|
||||||
|
|
||||||
|
/** A server-log line already rendered in TS3's HTML log format. */
|
||||||
|
default void onServerLogHtml(String html) {
|
||||||
|
onServerLog(html == null ? "" : html);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.github.manevolent.ts3j.api.Channel;
|
|||||||
import com.github.manevolent.ts3j.api.Client;
|
import com.github.manevolent.ts3j.api.Client;
|
||||||
import com.github.manevolent.ts3j.command.MultiCommand;
|
import com.github.manevolent.ts3j.command.MultiCommand;
|
||||||
import com.github.manevolent.ts3j.command.SingleCommand;
|
import com.github.manevolent.ts3j.command.SingleCommand;
|
||||||
|
import com.github.manevolent.ts3j.command.parameter.CommandParameter;
|
||||||
import com.github.manevolent.ts3j.command.parameter.CommandSingleParameter;
|
import com.github.manevolent.ts3j.command.parameter.CommandSingleParameter;
|
||||||
import com.github.manevolent.ts3j.event.*;
|
import com.github.manevolent.ts3j.event.*;
|
||||||
import com.github.manevolent.ts3j.identity.LocalIdentity;
|
import com.github.manevolent.ts3j.identity.LocalIdentity;
|
||||||
@@ -13,6 +14,8 @@ import com.github.manevolent.ts3j.util.Ts3Crypt;
|
|||||||
import com.ts3client.audio.AudioBackend;
|
import com.ts3client.audio.AudioBackend;
|
||||||
import com.ts3client.audio.VoiceInput;
|
import com.ts3client.audio.VoiceInput;
|
||||||
import com.ts3client.audio.VoiceOutput;
|
import com.ts3client.audio.VoiceOutput;
|
||||||
|
import com.ts3client.chatlog.ChatLogScope;
|
||||||
|
import com.ts3client.chatlog.ChatLogStore;
|
||||||
import com.ts3client.config.Settings;
|
import com.ts3client.config.Settings;
|
||||||
import com.ts3client.net.filetransfer.FileTransfer;
|
import com.ts3client.net.filetransfer.FileTransfer;
|
||||||
import com.ts3client.net.filetransfer.FileTransferManager;
|
import com.ts3client.net.filetransfer.FileTransferManager;
|
||||||
@@ -22,8 +25,10 @@ import com.ts3client.sound.SoundNotifier;
|
|||||||
import com.ts3client.text.TsLink;
|
import com.ts3client.text.TsLink;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -72,6 +77,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
private VoiceOutput playback;
|
private VoiceOutput playback;
|
||||||
private LocalIdentity identity;
|
private LocalIdentity identity;
|
||||||
private FileTransferManager fileTransfers;
|
private FileTransferManager fileTransfers;
|
||||||
|
private ChatLogStore chatLogs;
|
||||||
/**
|
/**
|
||||||
* Runs ts3j's event callbacks. Supplying it ourselves (rather than leaving ts3j to make
|
* Runs ts3j's event callbacks. Supplying it ourselves (rather than leaving ts3j to make
|
||||||
* its own) is what lets {@link #awaitEventsProcessed()} tell when everything the server
|
* its own) is what lets {@link #awaitEventsProcessed()} tell when everything the server
|
||||||
@@ -287,6 +293,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
leaving = false;
|
leaving = false;
|
||||||
disconnectAnnounced = false;
|
disconnectAnnounced = false;
|
||||||
deafened = false;
|
deafened = false;
|
||||||
|
loadChatLogs();
|
||||||
ui.onConnected();
|
ui.onConnected();
|
||||||
requestPermissionNames();
|
requestPermissionNames();
|
||||||
|
|
||||||
@@ -517,6 +524,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
playback = null;
|
playback = null;
|
||||||
client = null;
|
client = null;
|
||||||
fileTransfers = null;
|
fileTransfers = null;
|
||||||
|
chatLogs = null;
|
||||||
selfClientId = -1;
|
selfClientId = -1;
|
||||||
stats.clear();
|
stats.clear();
|
||||||
|
|
||||||
@@ -749,6 +757,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
ClientEntry self = model.getClient(selfClientId);
|
ClientEntry self = model.getClient(selfClientId);
|
||||||
int cid = self != null ? self.channelId : 0;
|
int cid = self != null ? self.channelId : 0;
|
||||||
client.sendChannelMessage(cid, text);
|
client.sendChannelMessage(cid, text);
|
||||||
|
persistOutgoingChat(ConnectionListener.ChatScope.CHANNEL, 0, "", text);
|
||||||
sound(SoundEvent.CHAT_SENT_MESSAGE_CHANNEL, channelVars(cid, null));
|
sound(SoundEvent.CHAT_SENT_MESSAGE_CHANNEL, channelVars(cid, null));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
error("Message failed: " + rootMessage(e));
|
error("Message failed: " + rootMessage(e));
|
||||||
@@ -760,6 +769,7 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
client.sendServerMessage(text);
|
client.sendServerMessage(text);
|
||||||
|
persistOutgoingChat(ConnectionListener.ChatScope.SERVER, 0, "", text);
|
||||||
sound(SoundEvent.CHAT_SENT_MESSAGE_SERVER, serverVars());
|
sound(SoundEvent.CHAT_SENT_MESSAGE_SERVER, serverVars());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
error("Message failed: " + rootMessage(e));
|
error("Message failed: " + rootMessage(e));
|
||||||
@@ -771,6 +781,16 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
client.sendPrivateMessage(clientId, text);
|
client.sendPrivateMessage(clientId, text);
|
||||||
|
ClientEntry peer = model.getClient(clientId);
|
||||||
|
String peerUid = peer != null ? peer.uniqueId : "";
|
||||||
|
if (peerUid == null || peerUid.isBlank()) {
|
||||||
|
try {
|
||||||
|
Client info = client.getClientInfo(clientId);
|
||||||
|
if (info != null) peerUid = info.getUniqueIdentifier();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
persistOutgoingChat(ConnectionListener.ChatScope.PRIVATE, clientId, peerUid, text);
|
||||||
sound(SoundEvent.CHAT_SENT_MESSAGE_CLIENT, clientVars(clientId, null));
|
sound(SoundEvent.CHAT_SENT_MESSAGE_CLIENT, clientVars(clientId, null));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
error("Message failed: " + rootMessage(e));
|
error("Message failed: " + rootMessage(e));
|
||||||
@@ -1199,9 +1219,162 @@ public final class TeamspeakConnection implements TS3Listener {
|
|||||||
sounds.fire(event, deafened, variables);
|
sounds.fire(event, deafened, variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void loadChatLogs() {
|
||||||
|
try {
|
||||||
|
String uniqueId = serverUniqueId();
|
||||||
|
if (uniqueId == null) {
|
||||||
|
chatLogs = null;
|
||||||
|
ui.onStatus("Chat logs unavailable: server unique id was not reported");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ChatLogStore logs = new ChatLogStore(uniqueId);
|
||||||
|
chatLogs = logs;
|
||||||
|
logs.begin();
|
||||||
|
List<String> serverHistory = logs.serverHtmlHistory();
|
||||||
|
List<String> channelHistory = logs.channelHtmlHistory();
|
||||||
|
ui.onChatHistory(ConnectionListener.ChatScope.SERVER, serverHistory);
|
||||||
|
ui.onChatHistory(ConnectionListener.ChatScope.CHANNEL, channelHistory);
|
||||||
|
ui.onStatus("Loaded chat logs from " + logs.serverDir()
|
||||||
|
+ " (" + serverHistory.size() + " server, " + channelHistory.size() + " channel)");
|
||||||
|
} catch (Exception e) {
|
||||||
|
chatLogs = null;
|
||||||
|
ui.onStatus("Chat logs unavailable: " + rootMessage(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serverUniqueId() {
|
||||||
|
String uniqueId = validServerUniqueId(firstPresent(
|
||||||
|
client.getServerProperties().get("virtualserver_unique_identifier"),
|
||||||
|
client.getServerProperties().get("virtualserver_unique_id")));
|
||||||
|
if (uniqueId != null) return uniqueId;
|
||||||
|
|
||||||
|
uniqueId = serverUniqueIdFrom(new SingleCommand("serverinfo", ProtocolRole.CLIENT));
|
||||||
|
if (uniqueId != null) return uniqueId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
SingleCommand command = new SingleCommand("servergetvariables", ProtocolRole.CLIENT);
|
||||||
|
String serverId = client.getServerProperties().get("virtualserver_id");
|
||||||
|
if (serverId != null && !serverId.isBlank()) {
|
||||||
|
command.add(new CommandSingleParameter("virtualserver_id", serverId));
|
||||||
|
}
|
||||||
|
return serverUniqueIdFrom(command);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serverUniqueIdFrom(SingleCommand command) {
|
||||||
|
try {
|
||||||
|
for (SingleCommand response : client.executeCommand(command).get()) {
|
||||||
|
String uniqueId = validServerUniqueId(firstPresent(
|
||||||
|
parameterValue(response, "virtualserver_unique_identifier"),
|
||||||
|
parameterValue(response, "virtualserver_unique_id")));
|
||||||
|
if (uniqueId != null) return uniqueId;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String validServerUniqueId(String value) {
|
||||||
|
if (value == null || value.isBlank()) return null;
|
||||||
|
try {
|
||||||
|
byte[] decoded = Base64.getDecoder().decode(value);
|
||||||
|
if (decoded.length != 20) return null;
|
||||||
|
|
||||||
|
// TS3 server UIDs are SHA-1-sized binary identifiers represented as base64.
|
||||||
|
// Plain host strings are rejected before they can become log folder names.
|
||||||
|
boolean printable = true;
|
||||||
|
for (byte b : decoded) {
|
||||||
|
int c = b & 0xff;
|
||||||
|
if (c < 0x20 || c > 0x7e) {
|
||||||
|
printable = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return printable ? null : value;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstPresent(String... values) {
|
||||||
|
for (String value : values) {
|
||||||
|
if (value != null && !value.isBlank()) return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String parameterValue(SingleCommand command, String key) {
|
||||||
|
CommandParameter parameter = command.get(key);
|
||||||
|
return parameter == null ? null : parameter.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadPrivateChatHistory(int clientId, String uniqueId) {
|
||||||
|
ChatLogStore logs = chatLogs;
|
||||||
|
if (logs == null || uniqueId == null || uniqueId.isBlank()) return;
|
||||||
|
try {
|
||||||
|
ClientEntry c = model.getClient(clientId);
|
||||||
|
ui.onPrivateChatHistory(clientId, c != null ? c.nickname : "Client " + clientId,
|
||||||
|
logs.privateHtmlHistory(uniqueId));
|
||||||
|
} catch (IOException e) {
|
||||||
|
ui.onStatus("Private chat log unavailable: " + rootMessage(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void persistIncomingChat(ConnectionListener.ChatScope scope, int fromClientId, String fromUniqueId,
|
||||||
|
String fromName, String message) {
|
||||||
|
ChatLogStore logs = chatLogs;
|
||||||
|
if (logs == null) return;
|
||||||
|
try {
|
||||||
|
logs.appendMessage(toLogScope(scope), false, fromClientId, fromUniqueId, fromName, message);
|
||||||
|
} catch (IOException e) {
|
||||||
|
ui.onStatus("Could not write chat log: " + rootMessage(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persistOutgoingChat(ConnectionListener.ChatScope scope, int peerClientId, String peerUniqueId,
|
||||||
|
String message) {
|
||||||
|
ChatLogStore logs = chatLogs;
|
||||||
|
if (logs == null) return;
|
||||||
|
try {
|
||||||
|
ClientEntry self = model.getClient(selfClientId);
|
||||||
|
String uid = identity != null ? identity.getUid().toBase64() : "";
|
||||||
|
String name = self != null ? self.nickname : settings.nickname;
|
||||||
|
if (scope == ConnectionListener.ChatScope.PRIVATE) {
|
||||||
|
if (peerUniqueId == null || peerUniqueId.isBlank()) return;
|
||||||
|
logs.appendPrivateMessage(peerUniqueId, true, selfClientId, uid, name, message);
|
||||||
|
} else {
|
||||||
|
logs.appendMessage(toLogScope(scope), true, selfClientId, uid, name, message);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
ui.onStatus("Could not write chat log: " + rootMessage(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ChatLogScope toLogScope(ConnectionListener.ChatScope scope) {
|
||||||
|
return switch (scope) {
|
||||||
|
case SERVER -> ChatLogScope.SERVER;
|
||||||
|
case CHANNEL -> ChatLogScope.CHANNEL;
|
||||||
|
case PRIVATE -> ChatLogScope.PRIVATE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Records a line in the server tab's log, the way native TS3 reports server activity. */
|
/** Records a line in the server tab's log, the way native TS3 reports server activity. */
|
||||||
void log(String message) {
|
void log(String message) {
|
||||||
if (connected) ui.onServerLog(message);
|
if (!connected) return;
|
||||||
|
ChatLogStore logs = chatLogs;
|
||||||
|
if (logs == null) {
|
||||||
|
ui.onServerLog(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ui.onServerLogHtml(logs.appendServerEvent(message));
|
||||||
|
} catch (IOException e) {
|
||||||
|
ui.onServerLog(message);
|
||||||
|
ui.onStatus("Could not write server log: " + rootMessage(e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String channelName(int channelId) {
|
String channelName(int channelId) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import java.io.IOException;
|
|||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,13 +139,30 @@ public final class ChatPanel extends JPanel {
|
|||||||
|
|
||||||
/** Opens (or focuses) the private chat with {@code clientId}. */
|
/** Opens (or focuses) the private chat with {@code clientId}. */
|
||||||
public void openPrivateChat(int clientId, String nickname) {
|
public void openPrivateChat(int clientId, String nickname) {
|
||||||
|
openPrivateChat(clientId, "", nickname);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Opens (or focuses) the private chat with {@code clientId}. */
|
||||||
|
public void openPrivateChat(int clientId, String uniqueId, String nickname) {
|
||||||
edt(() -> {
|
edt(() -> {
|
||||||
Tab tab = privateTab(clientId, nickname);
|
Tab tab = privateTab(clientId, uniqueId, nickname);
|
||||||
tabs.setSelectedComponent(tab.scroll);
|
tabs.setSelectedComponent(tab.scroll);
|
||||||
input.requestFocusInWindow();
|
input.requestFocusInWindow();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setServerHistory(List<String> htmlEntries) {
|
||||||
|
edt(() -> serverTab.setHistory(htmlEntries));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChannelHistory(List<String> htmlEntries) {
|
||||||
|
edt(() -> channelTab.setHistory(htmlEntries));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrivateHistory(int clientId, String nickname, List<String> htmlEntries) {
|
||||||
|
edt(() -> privateTab(clientId, "", nickname).setHistory(htmlEntries));
|
||||||
|
}
|
||||||
|
|
||||||
/** Drops every private chat tab, e.g. after disconnecting. */
|
/** Drops every private chat tab, e.g. after disconnecting. */
|
||||||
public void closePrivateChats() {
|
public void closePrivateChats() {
|
||||||
edt(() -> {
|
edt(() -> {
|
||||||
@@ -214,17 +232,35 @@ public final class ChatPanel extends JPanel {
|
|||||||
+ BBCode.linksToHtml(text) + "</span>"));
|
+ BBCode.linksToHtml(text) + "</span>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void appendServerLogHtml(String html) {
|
||||||
|
edt(() -> serverTab.appendRawHtml(html));
|
||||||
|
}
|
||||||
|
|
||||||
public void appendServerMessage(int fromId, String from, String text) {
|
public void appendServerMessage(int fromId, String from, String text) {
|
||||||
edt(() -> serverTab.appendMessage(fromId, from, text));
|
appendServerMessage(fromId, "", from, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void appendServerMessage(int fromId, String uniqueId, String from, String text) {
|
||||||
|
edt(() -> serverTab.appendMessage(fromId, uniqueId, from, text));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void appendChannelMessage(int fromId, String from, String text) {
|
public void appendChannelMessage(int fromId, String from, String text) {
|
||||||
edt(() -> channelTab.appendMessage(fromId, from, text));
|
appendChannelMessage(fromId, "", from, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void appendChannelMessage(int fromId, String uniqueId, String from, String text) {
|
||||||
|
edt(() -> channelTab.appendMessage(fromId, uniqueId, from, text));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Appends to the private chat with {@code peerId}, creating the tab if needed. */
|
/** Appends to the private chat with {@code peerId}, creating the tab if needed. */
|
||||||
public void appendPrivateMessage(int peerId, String peerName, int fromId, String from, String text) {
|
public void appendPrivateMessage(int peerId, String peerName, int fromId, String from, String text) {
|
||||||
edt(() -> privateTab(peerId, peerName).appendMessage(fromId, from, text));
|
appendPrivateMessage(peerId, "", peerName, fromId, "", from, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Appends to the private chat with {@code peerId}, creating the tab if needed. */
|
||||||
|
public void appendPrivateMessage(int peerId, String peerUniqueId, String peerName,
|
||||||
|
int fromId, String fromUniqueId, String from, String text) {
|
||||||
|
edt(() -> privateTab(peerId, peerUniqueId, peerName).appendMessage(fromId, fromUniqueId, from, text));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String stamp() {
|
private String stamp() {
|
||||||
@@ -232,11 +268,17 @@ public final class ChatPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Tab privateTab(int clientId, String nickname) {
|
private Tab privateTab(int clientId, String nickname) {
|
||||||
|
return privateTab(clientId, "", nickname);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Tab privateTab(int clientId, String uniqueId, String nickname) {
|
||||||
Tab tab = privateTabs.get(clientId);
|
Tab tab = privateTabs.get(clientId);
|
||||||
if (tab == null) {
|
if (tab == null) {
|
||||||
tab = new Tab(Target.PRIVATE, clientId, nickname, Icons.privateChat());
|
tab = new Tab(Target.PRIVATE, clientId, uniqueId, nickname, Icons.privateChat());
|
||||||
privateTabs.put(clientId, tab);
|
privateTabs.put(clientId, tab);
|
||||||
addTab(tab, true);
|
addTab(tab, true);
|
||||||
|
} else {
|
||||||
|
tab.setClientUniqueId(uniqueId);
|
||||||
}
|
}
|
||||||
return tab;
|
return tab;
|
||||||
}
|
}
|
||||||
@@ -301,6 +343,7 @@ public final class ChatPanel extends JPanel {
|
|||||||
|
|
||||||
final Target target;
|
final Target target;
|
||||||
final int clientId;
|
final int clientId;
|
||||||
|
private String clientUniqueId;
|
||||||
private String title;
|
private String title;
|
||||||
final JEditorPane log = HtmlStyles.pane("font-family:sans-serif; font-size:11px; margin:4px 6px;");
|
final JEditorPane log = HtmlStyles.pane("font-family:sans-serif; font-size:11px; margin:4px 6px;");
|
||||||
final JScrollPane scroll;
|
final JScrollPane scroll;
|
||||||
@@ -311,17 +354,23 @@ public final class ChatPanel extends JPanel {
|
|||||||
private JLabel titleLabel;
|
private JLabel titleLabel;
|
||||||
|
|
||||||
Tab(Target target, int clientId, String title, Icon icon) {
|
Tab(Target target, int clientId, String title, Icon icon) {
|
||||||
this(target, clientId, title, icon, null, null);
|
this(target, clientId, "", title, icon, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tab(Target target, int clientId, String clientUniqueId, String title, Icon icon) {
|
||||||
|
this(target, clientId, clientUniqueId, title, icon, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A static-content tab: no send target, closing it runs {@code onClose}. */
|
/** A static-content tab: no send target, closing it runs {@code onClose}. */
|
||||||
Tab(String noteKey, Icon icon, String title, Runnable onClose) {
|
Tab(String noteKey, Icon icon, String title, Runnable onClose) {
|
||||||
this(null, 0, title, icon, noteKey, onClose);
|
this(null, 0, "", title, icon, noteKey, onClose);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Tab(Target target, int clientId, String title, Icon icon, String noteKey, Runnable onClose) {
|
private Tab(Target target, int clientId, String clientUniqueId, String title,
|
||||||
|
Icon icon, String noteKey, Runnable onClose) {
|
||||||
this.target = target;
|
this.target = target;
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
|
this.clientUniqueId = clientUniqueId == null ? "" : clientUniqueId;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.icon = icon;
|
this.icon = icon;
|
||||||
this.noteKey = noteKey;
|
this.noteKey = noteKey;
|
||||||
@@ -338,6 +387,10 @@ public final class ChatPanel extends JPanel {
|
|||||||
scroll.setBorder(BorderFactory.createEmptyBorder());
|
scroll.setBorder(BorderFactory.createEmptyBorder());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setClientUniqueId(String uniqueId) {
|
||||||
|
if (uniqueId != null && !uniqueId.isBlank()) clientUniqueId = uniqueId;
|
||||||
|
}
|
||||||
|
|
||||||
private void onLink(String href) {
|
private void onLink(String href) {
|
||||||
if (href == null) return;
|
if (href == null) return;
|
||||||
TsLink.Ref ref = TsLink.parse(href);
|
TsLink.Ref ref = TsLink.parse(href);
|
||||||
@@ -405,16 +458,27 @@ public final class ChatPanel extends JPanel {
|
|||||||
titleLabel.setForeground(unread ? Theme.accent() : null);
|
titleLabel.setForeground(unread ? Theme.accent() : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
void appendMessage(int fromId, String from, String text) {
|
void appendMessage(int fromId, String fromUniqueId, String from, String text) {
|
||||||
String name = BBCode.escape(from);
|
String name = BBCode.escape(from);
|
||||||
String sender = fromId > 0
|
String sender = fromId > 0
|
||||||
? "<a class=\"" + BBCode.IDENTITY_LINK_CLASS + "\" href=\""
|
? "<a class=\"" + BBCode.IDENTITY_LINK_CLASS + "\" href=\""
|
||||||
+ BBCode.escape(TsLink.clientHref(fromId, "", from)) + "\">" + name + "</a>"
|
+ BBCode.escape(TsLink.clientHref(fromId,
|
||||||
|
fromUniqueId == null || fromUniqueId.isBlank() ? clientUniqueId : fromUniqueId,
|
||||||
|
from)) + "\">" + name + "</a>"
|
||||||
: "<span class=\"name\">" + name + "</span>";
|
: "<span class=\"name\">" + name + "</span>";
|
||||||
appendLine("<span class=\"muted\">" + stamp() + "</span>"
|
appendLine("<span class=\"muted\">" + stamp() + "</span>"
|
||||||
+ sender + ": " + BBCode.toHtml(text));
|
+ sender + ": " + BBCode.toHtml(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setHistory(List<String> htmlEntries) {
|
||||||
|
StringBuilder html = new StringBuilder();
|
||||||
|
if (htmlEntries != null) {
|
||||||
|
for (String entry : htmlEntries) html.append(normalizeTs3LogEntry(entry));
|
||||||
|
}
|
||||||
|
log.setText("<html><body><div id=\"chatlog\">" + html + "</div></body></html>");
|
||||||
|
log.setCaretPosition(log.getDocument().getLength());
|
||||||
|
}
|
||||||
|
|
||||||
/** Replaces the whole log with fixed content; used by static-content tabs. */
|
/** Replaces the whole log with fixed content; used by static-content tabs. */
|
||||||
void setStaticContent(String html) {
|
void setStaticContent(String html) {
|
||||||
log.setText("<html><body><div id=\"chatlog\">" + html + "</div></body></html>");
|
log.setText("<html><body><div id=\"chatlog\">" + html + "</div></body></html>");
|
||||||
@@ -422,13 +486,26 @@ public final class ChatPanel extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void appendLine(String html) {
|
void appendLine(String html) {
|
||||||
|
appendRawHtml("<div>" + html + "</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
void appendRawHtml(String html) {
|
||||||
try {
|
try {
|
||||||
HTMLDocument doc = (HTMLDocument) log.getDocument();
|
HTMLDocument doc = (HTMLDocument) log.getDocument();
|
||||||
doc.insertBeforeEnd(doc.getElement("chatlog"), "<div>" + html + "</div>");
|
doc.insertBeforeEnd(doc.getElement("chatlog"), normalizeTs3LogEntry(html));
|
||||||
log.setCaretPosition(doc.getLength());
|
log.setCaretPosition(doc.getLength());
|
||||||
} catch (BadLocationException | IOException ignored) {
|
} catch (BadLocationException | IOException ignored) {
|
||||||
}
|
}
|
||||||
if (tabs.getSelectedComponent() != scroll) setUnread(true);
|
if (tabs.getSelectedComponent() != scroll) setUnread(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String normalizeTs3LogEntry(String html) {
|
||||||
|
if (html == null || html.isBlank()) return "";
|
||||||
|
String normalized = html
|
||||||
|
.replaceAll("(?i)<img\\b[^>]*>", "")
|
||||||
|
.replaceFirst("(?i)^\\s*<p\\b", "<div")
|
||||||
|
.replaceFirst("(?i)</p>\\s*$", "</div>");
|
||||||
|
return normalized.startsWith("<div") ? normalized : "<div>" + normalized + "</div>";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.ts3client.net.TeamspeakConnection;
|
|||||||
|
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.SwingUtilities;
|
import javax.swing.SwingUtilities;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marshals {@link ConnectionListener} callbacks onto the EDT and fans them out to
|
* Marshals {@link ConnectionListener} callbacks onto the EDT and fans them out to
|
||||||
@@ -129,18 +130,34 @@ final class ServerTabConnectionEvents implements ConnectionListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onChat(ChatScope scope, int fromClientId, String fromName, String message) {
|
public void onChat(ChatScope scope, int fromClientId, String fromUniqueId, String fromName, String message) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
switch (scope) {
|
switch (scope) {
|
||||||
case PRIVATE:
|
case PRIVATE:
|
||||||
chatPanel.appendPrivateMessage(fromClientId, fromName, fromClientId, fromName, message);
|
chatPanel.appendPrivateMessage(fromClientId, fromUniqueId, fromName,
|
||||||
|
fromClientId, fromUniqueId, fromName, message);
|
||||||
break;
|
break;
|
||||||
case SERVER:
|
case SERVER:
|
||||||
chatPanel.appendServerMessage(fromClientId, fromName, message);
|
chatPanel.appendServerMessage(fromClientId, fromUniqueId, fromName, message);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
chatPanel.appendChannelMessage(fromClientId, fromName, message);
|
chatPanel.appendChannelMessage(fromClientId, fromUniqueId, fromName, message);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChatHistory(ChatScope scope, List<String> htmlEntries) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
if (scope == ChatScope.SERVER) chatPanel.setServerHistory(htmlEntries);
|
||||||
|
else if (scope == ChatScope.CHANNEL) chatPanel.setChannelHistory(htmlEntries);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrivateChatHistory(int clientId, String nickname, List<String> htmlEntries) {
|
||||||
|
SwingUtilities.invokeLater(() -> chatPanel.setPrivateHistory(clientId, nickname, htmlEntries));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -165,6 +182,11 @@ final class ServerTabConnectionEvents implements ConnectionListener {
|
|||||||
SwingUtilities.invokeLater(() -> chatPanel.appendServerLog(message));
|
SwingUtilities.invokeLater(() -> chatPanel.appendServerLog(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServerLogHtml(String html) {
|
||||||
|
SwingUtilities.invokeLater(() -> chatPanel.appendServerLogHtml(html));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPoke(String fromName, String message) {
|
public void onPoke(String fromName, String message) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
|||||||
@@ -123,7 +123,8 @@ final class ServerTabTreeActions implements ServerTreePanel.Actions {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void openPrivateChat(ClientEntry client) {
|
public void openPrivateChat(ClientEntry client) {
|
||||||
chatPanel.openPrivateChat(client.id, client.nickname);
|
chatPanel.openPrivateChat(client.id, client.uniqueId, client.nickname);
|
||||||
|
conn.loadPrivateChatHistory(client.id, client.uniqueId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Reference in New Issue
Block a user