A lot of updates and added a new feature

canary
WolverinDEV 2020-12-18 17:06:38 +01:00
parent 001bececbe
commit 3412faf125
46 changed files with 1568 additions and 773 deletions

View File

@ -1,4 +1,11 @@
# Changelog: # Changelog:
* **18.12.20**
- Added the ability to send private messages to multiple clients
- Channel client count now updates within the side bar header
- The client now supports the new server channel sidebar mode (File transfer is in progress)
- Correctly parsing and render `client://` urls
- Updating the client talk status if the client has been moved
* **13.12.20** * **13.12.20**
- Directly connection when hitting enter on the address line - Directly connection when hitting enter on the address line

View File

@ -134,6 +134,8 @@ html:root {
min-height: 0; min-height: 0;
width: 100%; width: 100%;
overflow: hidden;
border-radius: 5px 5px 0 0; border-radius: 5px 5px 0 0;
padding-right: 5px; padding-right: 5px;

View File

@ -24,8 +24,6 @@ import {FileTransferState, TransferProvider} from "./file/Transfer";
import {traj, tr} from "./i18n/localize"; import {traj, tr} from "./i18n/localize";
import {md5} from "./crypto/md5"; import {md5} from "./crypto/md5";
import {guid} from "./crypto/uid"; import {guid} from "./crypto/uid";
import {ServerEventLog} from "./ui/frames/log/ServerEventLog";
import {EventType} from "./ui/frames/log/Definitions";
import {PluginCmdRegistry} from "./connection/PluginCmdHandler"; import {PluginCmdRegistry} from "./connection/PluginCmdHandler";
import {W2GPluginCmdHandler} from "./video-viewer/W2GPlugin"; import {W2GPluginCmdHandler} from "./video-viewer/W2GPlugin";
import {VoiceConnectionStatus, WhisperSessionInitializeData} from "./connection/VoiceConnection"; import {VoiceConnectionStatus, WhisperSessionInitializeData} from "./connection/VoiceConnection";
@ -41,6 +39,8 @@ import {ChannelConversationManager} from "./conversations/ChannelConversationMan
import {PrivateConversationManager} from "tc-shared/conversations/PrivateConversationManager"; import {PrivateConversationManager} from "tc-shared/conversations/PrivateConversationManager";
import {SelectedClientInfo} from "./SelectedClientInfo"; import {SelectedClientInfo} from "./SelectedClientInfo";
import {SideBarManager} from "tc-shared/SideBarManager"; import {SideBarManager} from "tc-shared/SideBarManager";
import {ServerEventLog} from "tc-shared/connectionlog/ServerEventLog";
import {EventType} from "tc-shared/connectionlog/Definitions";
export enum InputHardwareState { export enum InputHardwareState {
MISSING, MISSING,
@ -270,7 +270,7 @@ export class ConnectionHandler {
} }
} }
log.info(LogCategory.CLIENT, tr("Start connection to %s:%d"), server_address.host, server_address.port); log.info(LogCategory.CLIENT, tr("Start connection to %s:%d"), server_address.host, server_address.port);
this.log.log(EventType.CONNECTION_BEGIN, { this.log.log("connection.begin", {
address: { address: {
server_hostname: server_address.host, server_hostname: server_address.host,
server_port: server_address.port server_port: server_address.port
@ -303,7 +303,7 @@ export class ConnectionHandler {
server_address.host = "127.0.0.1"; server_address.host = "127.0.0.1";
} else if(dns.supported() && !server_address.host.match(Regex.IP_V4) && !server_address.host.match(Regex.IP_V6)) { } else if(dns.supported() && !server_address.host.match(Regex.IP_V4) && !server_address.host.match(Regex.IP_V6)) {
const id = ++this._connect_initialize_id; const id = ++this._connect_initialize_id;
this.log.log(EventType.CONNECTION_HOSTNAME_RESOLVE, {}); this.log.log("connection.hostname.resolve", {});
try { try {
const resolved = await dns.resolve_address(server_address, { timeout: 5000 }) || {} as any; const resolved = await dns.resolve_address(server_address, { timeout: 5000 }) || {} as any;
if(id != this._connect_initialize_id) if(id != this._connect_initialize_id)
@ -311,7 +311,7 @@ export class ConnectionHandler {
server_address.host = typeof(resolved.target_ip) === "string" ? resolved.target_ip : server_address.host; server_address.host = typeof(resolved.target_ip) === "string" ? resolved.target_ip : server_address.host;
server_address.port = typeof(resolved.target_port) === "number" ? resolved.target_port : server_address.port; server_address.port = typeof(resolved.target_port) === "number" ? resolved.target_port : server_address.port;
this.log.log(EventType.CONNECTION_HOSTNAME_RESOLVED, { this.log.log("connection.hostname.resolved", {
address: { address: {
server_port: server_address.port, server_port: server_address.port,
server_hostname: server_address.host server_hostname: server_address.host
@ -348,7 +348,7 @@ export class ConnectionHandler {
log.warn(LogCategory.CLIENT, tr("Failed to successfully disconnect from server: {}"), error); log.warn(LogCategory.CLIENT, tr("Failed to successfully disconnect from server: {}"), error);
} }
this.sound.play(Sound.CONNECTION_DISCONNECTED); this.sound.play(Sound.CONNECTION_DISCONNECTED);
this.log.log(EventType.DISCONNECTED, {}); this.log.log("disconnected", {});
} }
getClient() : LocalClientEntry { return this.localClient; } getClient() : LocalClientEntry { return this.localClient; }
@ -386,7 +386,7 @@ export class ConnectionHandler {
this.connection_state = event.newState; this.connection_state = event.newState;
if(event.newState === ConnectionState.CONNECTED) { if(event.newState === ConnectionState.CONNECTED) {
log.info(LogCategory.CLIENT, tr("Client connected")); log.info(LogCategory.CLIENT, tr("Client connected"));
this.log.log(EventType.CONNECTION_CONNECTED, { this.log.log("connection.connected", {
serverAddress: { serverAddress: {
server_port: this.channelTree.server.remote_address.port, server_port: this.channelTree.server.remote_address.port,
server_hostname: this.channelTree.server.remote_address.host server_hostname: this.channelTree.server.remote_address.host
@ -487,12 +487,12 @@ export class ConnectionHandler {
case DisconnectReason.HANDLER_DESTROYED: case DisconnectReason.HANDLER_DESTROYED:
if(data) { if(data) {
this.sound.play(Sound.CONNECTION_DISCONNECTED); this.sound.play(Sound.CONNECTION_DISCONNECTED);
this.log.log(EventType.DISCONNECTED, {}); this.log.log("disconnected", {});
} }
break; break;
case DisconnectReason.DNS_FAILED: case DisconnectReason.DNS_FAILED:
log.error(LogCategory.CLIENT, tr("Failed to resolve hostname: %o"), data); log.error(LogCategory.CLIENT, tr("Failed to resolve hostname: %o"), data);
this.log.log(EventType.CONNECTION_HOSTNAME_RESOLVE_ERROR, { this.log.log("connection.hostname.resolve.error", {
message: data as any message: data as any
}); });
this.sound.play(Sound.CONNECTION_REFUSED); this.sound.play(Sound.CONNECTION_REFUSED);
@ -539,7 +539,7 @@ export class ConnectionHandler {
this._certificate_modal.open(); this._certificate_modal.open();
}); });
} }
this.log.log(EventType.CONNECTION_FAILED, { this.log.log("connection.failed", {
serverAddress: { serverAddress: {
server_hostname: this.serverConnection.remote_address().host, server_hostname: this.serverConnection.remote_address().host,
server_port: this.serverConnection.remote_address().port server_port: this.serverConnection.remote_address().port
@ -594,7 +594,7 @@ export class ConnectionHandler {
break; break;
case DisconnectReason.SERVER_CLOSED: case DisconnectReason.SERVER_CLOSED:
this.log.log(EventType.SERVER_CLOSED, {message: data.reasonmsg}); this.log.log("server.closed", {message: data.reasonmsg});
createErrorModal( createErrorModal(
tr("Server closed"), tr("Server closed"),
@ -606,7 +606,7 @@ export class ConnectionHandler {
auto_reconnect = true; auto_reconnect = true;
break; break;
case DisconnectReason.SERVER_REQUIRES_PASSWORD: case DisconnectReason.SERVER_REQUIRES_PASSWORD:
this.log.log(EventType.SERVER_REQUIRES_PASSWORD, {}); this.log.log("server.requires.password", {});
createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => { createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => {
if(!(typeof password === "string")) return; if(!(typeof password === "string")) return;
@ -647,7 +647,7 @@ export class ConnectionHandler {
this.sound.play(Sound.CONNECTION_BANNED); this.sound.play(Sound.CONNECTION_BANNED);
break; break;
case DisconnectReason.CLIENT_BANNED: case DisconnectReason.CLIENT_BANNED:
this.log.log(EventType.SERVER_BANNED, { this.log.log("server.banned", {
invoker: { invoker: {
client_name: data["invokername"], client_name: data["invokername"],
client_id: parseInt(data["invokerid"]), client_id: parseInt(data["invokerid"]),
@ -678,7 +678,7 @@ export class ConnectionHandler {
log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect but cant reconnect because we dont have any information left...")); log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect but cant reconnect because we dont have any information left..."));
return; return;
} }
this.log.log(EventType.RECONNECT_SCHEDULED, {timeout: 5000}); this.log.log("reconnect.scheduled", {timeout: 5000});
log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect. Reconnecting in 5000ms")); log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect. Reconnecting in 5000ms"));
const server_address = this.serverConnection.remote_address(); const server_address = this.serverConnection.remote_address();
@ -686,7 +686,7 @@ export class ConnectionHandler {
this._reconnect_timer = setTimeout(() => { this._reconnect_timer = setTimeout(() => {
this._reconnect_timer = undefined; this._reconnect_timer = undefined;
this.log.log(EventType.RECONNECT_EXECUTE, {}); this.log.log("reconnect.execute", {});
log.info(LogCategory.NETWORKING, tr("Reconnecting...")); log.info(LogCategory.NETWORKING, tr("Reconnecting..."));
this.startConnection(server_address.host + ":" + server_address.port, profile, false, Object.assign(this.reconnect_properties(profile), {auto_reconnect_attempt: true})); this.startConnection(server_address.host + ":" + server_address.port, profile, false, Object.assign(this.reconnect_properties(profile), {auto_reconnect_attempt: true}));
@ -698,7 +698,7 @@ export class ConnectionHandler {
cancel_reconnect(log_event: boolean) { cancel_reconnect(log_event: boolean) {
if(this._reconnect_timer) { if(this._reconnect_timer) {
if(log_event) this.log.log(EventType.RECONNECT_CANCELED, {}); if(log_event) this.log.log("reconnect.canceled", {});
clearTimeout(this._reconnect_timer); clearTimeout(this._reconnect_timer);
this._reconnect_timer = undefined; this._reconnect_timer = undefined;
} }
@ -791,7 +791,7 @@ export class ConnectionHandler {
this.clientStatusSync = true; this.clientStatusSync = true;
this.serverConnection.send_command("clientupdate", localClientUpdates).catch(error => { this.serverConnection.send_command("clientupdate", localClientUpdates).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to update client audio hardware properties. Error: %o"), error); log.warn(LogCategory.GENERAL, tr("Failed to update client audio hardware properties. Error: %o"), error);
this.log.log(EventType.ERROR_CUSTOM, { message: tr("Failed to update audio hardware properties.") }); this.log.log("error.custom", { message: tr("Failed to update audio hardware properties.") });
this.clientStatusSync = false; this.clientStatusSync = false;
}); });
} }
@ -837,7 +837,7 @@ export class ConnectionHandler {
//client_output_hardware: this.client_status.sound_playback_supported //client_output_hardware: this.client_status.sound_playback_supported
}).catch(error => { }).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to sync handler state with server. Error: %o"), error); log.warn(LogCategory.GENERAL, tr("Failed to sync handler state with server. Error: %o"), error);
this.log.log(EventType.ERROR_CUSTOM, {message: tr("Failed to sync handler state with server.")}); this.log.log("error.custom", {message: tr("Failed to sync handler state with server.")});
}); });
} }
@ -1154,7 +1154,7 @@ export class ConnectionHandler {
client_away_message: typeof(this.client_status.away) === "string" ? this.client_status.away : "", client_away_message: typeof(this.client_status.away) === "string" ? this.client_status.away : "",
}).catch(error => { }).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to update away status. Error: %o"), error); log.warn(LogCategory.GENERAL, tr("Failed to update away status. Error: %o"), error);
this.log.log(EventType.ERROR_CUSTOM, {message: tr("Failed to update away status.")}); this.log.log("error.custom", {message: tr("Failed to update away status.")});
}); });
this.event_registry.fire("notify_state_updated", { this.event_registry.fire("notify_state_updated", {

View File

@ -6,6 +6,8 @@ import {FooterRenderer} from "tc-shared/ui/frames/footer/Renderer";
import * as React from "react"; import * as React from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import {SideBarController} from "tc-shared/ui/frames/SideBarController"; import {SideBarController} from "tc-shared/ui/frames/SideBarController";
import {ServerEventLogController} from "tc-shared/ui/frames/log/Controller";
import {ServerLogFrame} from "tc-shared/ui/frames/log/Renderer";
export let server_connections: ConnectionManager; export let server_connections: ConnectionManager;
@ -30,33 +32,36 @@ export class ConnectionManager {
private connection_handlers: ConnectionHandler[] = []; private connection_handlers: ConnectionHandler[] = [];
private active_handler: ConnectionHandler | undefined; private active_handler: ConnectionHandler | undefined;
private _container_log_server: JQuery;
private _container_channel_tree: JQuery; private _container_channel_tree: JQuery;
private _container_hostbanner: JQuery; private _container_hostbanner: JQuery;
private containerChannelVideo: ReplaceableContainer; private containerChannelVideo: ReplaceableContainer;
private containerSideBar: HTMLDivElement; private containerSideBar: HTMLDivElement;
private containerFooter: HTMLDivElement; private containerFooter: HTMLDivElement;
private containerServerLog: HTMLDivElement;
private sideBarController: SideBarController; private sideBarController: SideBarController;
private serverLogController: ServerEventLogController;
constructor() { constructor() {
this.event_registry = new Registry<ConnectionManagerEvents>(); this.event_registry = new Registry<ConnectionManagerEvents>();
this.event_registry.enableDebug("connection-manager"); this.event_registry.enableDebug("connection-manager");
this.sideBarController = new SideBarController(); this.sideBarController = new SideBarController();
this.serverLogController = new ServerEventLogController();
this.containerChannelVideo = new ReplaceableContainer(document.getElementById("channel-video") as HTMLDivElement); this.containerChannelVideo = new ReplaceableContainer(document.getElementById("channel-video") as HTMLDivElement);
this._container_log_server = $("#server-log"); this.containerServerLog = document.getElementById("server-log") as HTMLDivElement;
this.containerFooter = document.getElementById("container-footer") as HTMLDivElement;
this._container_channel_tree = $("#channelTree"); this._container_channel_tree = $("#channelTree");
this._container_hostbanner = $("#hostbanner"); this._container_hostbanner = $("#hostbanner");
this.containerFooter = document.getElementById("container-footer") as HTMLDivElement;
this.sideBarController.renderInto(document.getElementById("chat") as HTMLDivElement); this.sideBarController.renderInto(document.getElementById("chat") as HTMLDivElement);
this.set_active_connection(undefined); this.set_active_connection(undefined);
} }
initializeFooter() { initializeReactComponents() {
ReactDOM.render(React.createElement(FooterRenderer), this.containerFooter); ReactDOM.render(React.createElement(FooterRenderer), this.containerFooter);
ReactDOM.render(React.createElement(ServerLogFrame, { events: this.serverLogController.events }), this.containerServerLog);
} }
events() : Registry<ConnectionManagerEvents> { events() : Registry<ConnectionManagerEvents> {
@ -117,16 +122,15 @@ export class ConnectionManager {
private set_active_connection_(handler: ConnectionHandler) { private set_active_connection_(handler: ConnectionHandler) {
this.sideBarController.setConnection(handler); this.sideBarController.setConnection(handler);
this.serverLogController.setConnectionHandler(handler);
this._container_channel_tree.children().detach(); this._container_channel_tree.children().detach();
this._container_log_server.children().detach();
this._container_hostbanner.children().detach(); this._container_hostbanner.children().detach();
this.containerChannelVideo.replaceWith(handler?.video_frame.getContainer()); this.containerChannelVideo.replaceWith(handler?.video_frame.getContainer());
if(handler) { if(handler) {
this._container_hostbanner.append(handler.hostbanner.html_tag); this._container_hostbanner.append(handler.hostbanner.html_tag);
this._container_channel_tree.append(handler.channelTree.tag_tree()); this._container_channel_tree.append(handler.channelTree.tag_tree());
this._container_log_server.append(handler.log.getHTMLTag());
} }
const old_handler = this.active_handler; const old_handler = this.active_handler;
@ -184,7 +188,7 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "server manager init", name: "server manager init",
function: async () => { function: async () => {
server_connections = new ConnectionManager(); server_connections = new ConnectionManager();
server_connections.initializeFooter(); server_connections.initializeReactComponents();
}, },
priority: 80 priority: 80
}); });

View File

@ -16,7 +16,7 @@ export class SideBarManager {
constructor(connection: ConnectionHandler) { constructor(connection: ConnectionHandler) {
this.events = new Registry<SideBarManagerEvents>(); this.events = new Registry<SideBarManagerEvents>();
this.connection = connection; this.connection = connection;
this.currentType = "channel-chat"; this.currentType = "channel";
} }
destroy() {} destroy() {}
@ -38,8 +38,8 @@ export class SideBarManager {
this.setSideBarContent("private-chat"); this.setSideBarContent("private-chat");
} }
showChannelConversations() { showChannel() {
this.setSideBarContent("channel-chat"); this.setSideBarContent("channel");
} }
showClientInfo(client: ClientEntry) { showClientInfo(client: ClientEntry) {

View File

@ -1,5 +1,5 @@
import * as log from "../log"; import * as log from "../log";
import {LogCategory, logError} from "../log"; import {LogCategory, logError, logWarn} from "../log";
import {AbstractServerConnection, CommandOptions, ServerCommand} from "../connection/ConnectionBase"; import {AbstractServerConnection, CommandOptions, ServerCommand} from "../connection/ConnectionBase";
import {Sound} from "../sound/Sounds"; import {Sound} from "../sound/Sounds";
import {CommandResult} from "../connection/ServerConnectionDeclaration"; import {CommandResult} from "../connection/ServerConnectionDeclaration";
@ -20,10 +20,10 @@ import {batch_updates, BatchUpdateType, flush_batched_updates} from "../ui/react
import {OutOfViewClient} from "../ui/frames/side/PrivateConversationController"; import {OutOfViewClient} from "../ui/frames/side/PrivateConversationController";
import {renderBBCodeAsJQuery} from "../text/bbcode"; import {renderBBCodeAsJQuery} from "../text/bbcode";
import {tr} from "../i18n/localize"; import {tr} from "../i18n/localize";
import {EventClient, EventType} from "../ui/frames/log/Definitions";
import {ErrorCode} from "../connection/ErrorCode"; import {ErrorCode} from "../connection/ErrorCode";
import {server_connections} from "tc-shared/ConnectionManager"; import {server_connections} from "tc-shared/ConnectionManager";
import {ChannelEntry} from "tc-shared/tree/Channel"; import {ChannelEntry} from "tc-shared/tree/Channel";
import {EventClient} from "tc-shared/connectionlog/Definitions";
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss { export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
constructor(connection: AbstractServerConnection) { constructor(connection: AbstractServerConnection) {
@ -56,6 +56,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
this["initserver"] = this.handleCommandServerInit; this["initserver"] = this.handleCommandServerInit;
this["notifychannelmoved"] = this.handleNotifyChannelMoved; this["notifychannelmoved"] = this.handleNotifyChannelMoved;
this["notifychanneledited"] = this.handleNotifyChannelEdited; this["notifychanneledited"] = this.handleNotifyChannelEdited;
this["notifychanneldescriptionchanged"] = this.handleNotifyChannelDescriptionChanged;
this["notifytextmessage"] = this.handleNotifyTextMessage; this["notifytextmessage"] = this.handleNotifyTextMessage;
this["notifyclientchatcomposing"] = this.notifyClientChatComposing; this["notifyclientchatcomposing"] = this.notifyClientChatComposing;
this["notifyclientchatclosed"] = this.handleNotifyClientChatClosed; this["notifyclientchatclosed"] = this.handleNotifyClientChatClosed;
@ -116,18 +117,18 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
if(res.id == ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) { //Permission error if(res.id == ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) { //Permission error
const permission = this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number); const permission = this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number);
res.message = tr("Insufficient client permissions. Failed on permission ") + (permission ? permission.name : "unknown"); res.message = tr("Insufficient client permissions. Failed on permission ") + (permission ? permission.name : "unknown");
this.connection_handler.log.log(EventType.ERROR_PERMISSION, { this.connection_handler.log.log("error.permission", {
permission: this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number) permission: this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number)
}); });
this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
} else if(res.id != ErrorCode.DATABASE_EMPTY_RESULT) { } else if(res.id != ErrorCode.DATABASE_EMPTY_RESULT) {
this.connection_handler.log.log(EventType.ERROR_CUSTOM, { this.connection_handler.log.log("error.custom", {
message: res.extra_message.length == 0 ? res.message : res.extra_message message: res.extra_message.length == 0 ? res.message : res.extra_message
}); });
} }
} }
} else if(typeof(ex) === "string") { } else if(typeof(ex) === "string") {
this.connection_handler.log.log(EventType.CONNECTION_COMMAND_ERROR, {error: ex}); this.connection_handler.log.log("connection.command.error", {error: ex});
} else { } else {
log.error(LogCategory.NETWORKING, tr("Invalid promise result type: %s. Result: %o"), typeof (ex), ex); log.error(LogCategory.NETWORKING, tr("Invalid promise result type: %s. Result: %o"), typeof (ex), ex);
} }
@ -204,7 +205,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
if(properties.virtualserver_hostmessage_mode == 1) { if(properties.virtualserver_hostmessage_mode == 1) {
/* show in log */ /* show in log */
if(properties.virtualserver_hostmessage) if(properties.virtualserver_hostmessage)
this.connection_handler.log.log(EventType.SERVER_HOST_MESSAGE, { this.connection_handler.log.log("server.host.message", {
message: properties.virtualserver_hostmessage message: properties.virtualserver_hostmessage
}); });
} else { } else {
@ -219,7 +220,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
if(properties.virtualserver_hostmessage_mode == 3) { if(properties.virtualserver_hostmessage_mode == 3) {
/* first let the client initialize his stuff */ /* first let the client initialize his stuff */
setTimeout(() => { setTimeout(() => {
this.connection_handler.log.log(EventType.SERVER_HOST_MESSAGE_DISCONNECT, { this.connection_handler.log.log("server.host.message.disconnect", {
message: properties.virtualserver_welcomemessage message: properties.virtualserver_welcomemessage
}); });
@ -233,7 +234,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
/* welcome message */ /* welcome message */
if(properties.virtualserver_welcomemessage) { if(properties.virtualserver_welcomemessage) {
this.connection_handler.log.log(EventType.SERVER_WELCOME_MESSAGE, { this.connection_handler.log.log("server.welcome.message", {
message: properties.virtualserver_welcomemessage message: properties.virtualserver_welcomemessage
}); });
} }
@ -504,7 +505,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
if(this.connection_handler.areQueriesShown() || client.properties.client_type != ClientType.CLIENT_QUERY) { if(this.connection_handler.areQueriesShown() || client.properties.client_type != ClientType.CLIENT_QUERY) {
const own_channel = this.connection.client.getClient().currentChannel(); const own_channel = this.connection.client.getClient().currentChannel();
this.connection_handler.log.log(channel == own_channel ? EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL : EventType.CLIENT_VIEW_ENTER, { this.connection_handler.log.log(channel == own_channel ? "client.view.enter.own.channel" : "client.view.enter", {
channel_from: old_channel ? old_channel.log_data() : undefined, channel_from: old_channel ? old_channel.log_data() : undefined,
channel_to: channel ? channel.log_data() : undefined, channel_to: channel ? channel.log_data() : undefined,
client: client.log_data(), client: client.log_data(),
@ -592,7 +593,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
let channel_to = tree.findChannel(targetChannelId); let channel_to = tree.findChannel(targetChannelId);
const is_own_channel = channel_from == own_channel; const is_own_channel = channel_from == own_channel;
this.connection_handler.log.log(is_own_channel ? EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL : EventType.CLIENT_VIEW_LEAVE, { this.connection_handler.log.log(is_own_channel ? "client.view.leave.own.channel" : "client.view.leave", {
channel_from: channel_from ? channel_from.log_data() : undefined, channel_from: channel_from ? channel_from.log_data() : undefined,
channel_to: channel_to ? channel_to.log_data() : undefined, channel_to: channel_to ? channel_to.log_data() : undefined,
client: client.log_data(), client: client.log_data(),
@ -673,7 +674,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
} }
const own_channel = this.connection.client.getClient().currentChannel(); const own_channel = this.connection.client.getClient().currentChannel();
const event = self ? EventType.CLIENT_VIEW_MOVE_OWN : (channelFrom == own_channel || channel_to == own_channel ? EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL : EventType.CLIENT_VIEW_MOVE); const event = self ? "client.view.move.own" : (channelFrom == own_channel || channel_to == own_channel ? "client.view.move.own.channel" : "client.view.move");
this.connection_handler.log.log(event, { this.connection_handler.log.log(event, {
channel_from: channelFrom ? { channel_from: channelFrom ? {
channel_id: channelFrom.channelId, channel_id: channelFrom.channelId,
@ -779,6 +780,19 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
} }
} }
handleNotifyChannelDescriptionChanged(json) {
json = json[0];
let tree = this.connection.client.channelTree;
let channel = tree.findChannel(parseInt(json["cid"]));
if(!channel) {
logWarn(LogCategory.NETWORKING, tr("Received channel description changed notify for invalid channel: %o"), json["cid"]);
return;
}
channel.handleDescriptionChanged();
}
handleNotifyTextMessage(json) { handleNotifyTextMessage(json) {
json = json[0]; //Only one bulk json = json[0]; //Only one bulk
@ -815,7 +829,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
}); });
if(targetIsOwn) { if(targetIsOwn) {
this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5}); this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5});
this.connection_handler.log.log(EventType.PRIVATE_MESSAGE_RECEIVED, { this.connection_handler.log.log("private.message.received", {
message: json["msg"], message: json["msg"],
sender: { sender: {
client_unique_id: json["invokeruid"], client_unique_id: json["invokeruid"],
@ -825,7 +839,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
}); });
} else { } else {
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
this.connection_handler.log.log(EventType.PRIVATE_MESSAGE_SEND, { this.connection_handler.log.log("private.message.send", {
message: json["msg"], message: json["msg"],
target: { target: {
client_unique_id: json["invokeruid"], client_unique_id: json["invokeruid"],
@ -858,7 +872,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"]));
const conversations = this.connection_handler.getChannelConversations(); const conversations = this.connection_handler.getChannelConversations();
this.connection_handler.log.log(EventType.GLOBAL_MESSAGE, { this.connection_handler.log.log("global.message", {
isOwnMessage: invoker instanceof LocalClientEntry, isOwnMessage: invoker instanceof LocalClientEntry,
message: json["msg"], message: json["msg"],
sender: { sender: {
@ -976,7 +990,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
unique_id: json["invokeruid"] unique_id: json["invokeruid"]
}, json["msg"]); }, json["msg"]);
this.connection_handler.log.log(EventType.CLIENT_POKE_RECEIVED, { this.connection_handler.log.log("client.poke.received", {
sender: this.loggable_invoker(json["invokeruid"], json["invokerid"], json["invokername"]), sender: this.loggable_invoker(json["invokeruid"], json["invokerid"], json["invokername"]),
message: json["msg"] message: json["msg"]
}); });

View File

@ -172,6 +172,7 @@ export enum ErrorCode {
CUSTOM_ERROR = 0xFFFF, CUSTOM_ERROR = 0xFFFF,
/** @deprecated Use SERVER_INSUFFICIENT_PERMISSIONS */
PERMISSION_ERROR = ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS, PERMISSION_ERROR = ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS,
EMPTY_RESULT = ErrorCode.DATABASE_EMPTY_RESULT EMPTY_RESULT = ErrorCode.DATABASE_EMPTY_RESULT
} }

View File

@ -0,0 +1,345 @@
import * as React from "react";
import {ServerEventLog} from "tc-shared/connectionlog/ServerEventLog";
import {ViewReasonId} from "tc-shared/ConnectionHandler";
import {PermissionInfo} from "tc-shared/permission/PermissionManager";
/* FIXME: Remove this! */
export enum EventType {
CONNECTION_BEGIN = "connection.begin",
CONNECTION_HOSTNAME_RESOLVE = "connection.hostname.resolve",
CONNECTION_HOSTNAME_RESOLVE_ERROR = "connection.hostname.resolve.error",
CONNECTION_HOSTNAME_RESOLVED = "connection.hostname.resolved",
CONNECTION_LOGIN = "connection.login",
CONNECTION_CONNECTED = "connection.connected",
CONNECTION_FAILED = "connection.failed",
DISCONNECTED = "disconnected",
CONNECTION_VOICE_CONNECT = "connection.voice.connect",
CONNECTION_VOICE_CONNECT_FAILED = "connection.voice.connect.failed",
CONNECTION_VOICE_CONNECT_SUCCEEDED = "connection.voice.connect.succeeded",
CONNECTION_VOICE_DROPPED = "connection.voice.dropped",
CONNECTION_COMMAND_ERROR = "connection.command.error",
GLOBAL_MESSAGE = "global.message",
SERVER_WELCOME_MESSAGE = "server.welcome.message",
SERVER_HOST_MESSAGE = "server.host.message",
SERVER_HOST_MESSAGE_DISCONNECT = "server.host.message.disconnect",
SERVER_CLOSED = "server.closed",
SERVER_BANNED = "server.banned",
SERVER_REQUIRES_PASSWORD = "server.requires.password",
CLIENT_VIEW_ENTER = "client.view.enter",
CLIENT_VIEW_LEAVE = "client.view.leave",
CLIENT_VIEW_MOVE = "client.view.move",
CLIENT_VIEW_ENTER_OWN_CHANNEL = "client.view.enter.own.channel",
CLIENT_VIEW_LEAVE_OWN_CHANNEL = "client.view.leave.own.channel",
CLIENT_VIEW_MOVE_OWN_CHANNEL = "client.view.move.own.channel",
CLIENT_VIEW_MOVE_OWN = "client.view.move.own",
CLIENT_NICKNAME_CHANGED = "client.nickname.changed",
CLIENT_NICKNAME_CHANGED_OWN = "client.nickname.changed.own",
CLIENT_NICKNAME_CHANGE_FAILED = "client.nickname.change.failed",
CLIENT_SERVER_GROUP_ADD = "client.server.group.add",
CLIENT_SERVER_GROUP_REMOVE = "client.server.group.remove",
CLIENT_CHANNEL_GROUP_CHANGE = "client.channel.group.change",
PRIVATE_MESSAGE_RECEIVED = "private.message.received",
PRIVATE_MESSAGE_SEND = "private.message.send",
CHANNEL_CREATE = "channel.create",
CHANNEL_DELETE = "channel.delete",
ERROR_CUSTOM = "error.custom",
ERROR_PERMISSION = "error.permission",
CLIENT_POKE_RECEIVED = "client.poke.received",
CLIENT_POKE_SEND = "client.poke.send",
RECONNECT_SCHEDULED = "reconnect.scheduled",
RECONNECT_EXECUTE = "reconnect.execute",
RECONNECT_CANCELED = "reconnect.canceled",
WEBRTC_FATAL_ERROR = "webrtc.fatal.error"
}
export type EventClient = {
client_unique_id: string;
client_name: string;
client_id: number;
}
export type EventChannelData = {
channel_id: number;
channel_name: string;
}
export type EventServerAddress = {
server_hostname: string;
server_port: number;
}
export namespace event {
export type EventGlobalMessage = {
isOwnMessage: boolean;
sender: EventClient;
message: string;
}
export type EventConnectBegin = {
address: EventServerAddress;
client_nickname: string;
}
export type EventErrorCustom = {
message: string;
}
export type EventReconnectScheduled = {
timeout: number;
}
export type EventReconnectCanceled = { }
export type EventReconnectExecute = { }
export type EventErrorPermission = {
permission: PermissionInfo;
}
export type EventWelcomeMessage = {
message: string;
}
export type EventHostMessageDisconnect = {
message: string;
}
export type EventClientMove = {
channel_from?: EventChannelData;
channel_from_own: boolean;
channel_to?: EventChannelData;
channel_to_own: boolean;
client: EventClient;
client_own: boolean;
invoker?: EventClient;
message?: string;
reason: ViewReasonId;
}
export type EventClientEnter = {
channel_from?: EventChannelData;
channel_to?: EventChannelData;
client: EventClient;
invoker?: EventClient;
message?: string;
reason: ViewReasonId;
ban_time?: number;
}
export type EventClientLeave = {
channel_from?: EventChannelData;
channel_to?: EventChannelData;
client: EventClient;
invoker?: EventClient;
message?: string;
reason: ViewReasonId;
ban_time?: number;
}
export type EventChannelCreate = {
creator: EventClient,
channel: EventChannelData,
ownAction: boolean
}
export type EventChannelToggle = {
channel: EventChannelData
}
export type EventChannelDelete = {
deleter: EventClient,
channel: EventChannelData,
ownAction: boolean
}
export type EventConnectionConnected = {
serverName: string,
serverAddress: EventServerAddress,
own_client: EventClient;
}
export type EventConnectionFailed = {
serverAddress: EventServerAddress
}
export type EventConnectionLogin = {}
export type EventConnectionHostnameResolve = {};
export type EventConnectionHostnameResolved = {
address: EventServerAddress;
}
export type EventConnectionHostnameResolveError = {
message: string;
}
export type EventConnectionVoiceConnectFailed = {
reason: string;
reconnect_delay: number; /* if less or equal to 0 reconnect is prohibited */
}
export type EventConnectionVoiceConnectSucceeded = {}
export type EventConnectionVoiceConnect = {
attemptCount: number
}
export type EventConnectionVoiceDropped = {}
export type EventConnectionCommandError = {
error: any;
}
export type EventClientNicknameChanged = {
client: EventClient;
old_name: string;
new_name: string;
}
export type EventClientNicknameChangeFailed = {
reason: string;
}
export type EventServerClosed = {
message: string;
}
export type EventServerRequiresPassword = {}
export type EventServerBanned = {
message: string;
time: number;
invoker: EventClient;
}
export type EventClientPokeReceived = {
sender: EventClient,
message: string
}
export type EventClientPokeSend = {
target: EventClient,
message: string
}
export type EventPrivateMessageSend = {
target: EventClient,
message: string
}
export type EventPrivateMessageReceived = {
sender: EventClient,
message: string
}
export type EventWebrtcFatalError = {
message: string,
retryTimeout: number | 0
}
}
export type LogMessage = {
type: EventType;
uniqueId: string;
timestamp: number;
data: any;
}
export interface TypeInfo {
"connection.begin" : event.EventConnectBegin;
"global.message": event.EventGlobalMessage;
"error.custom": event.EventErrorCustom;
"error.permission": event.EventErrorPermission;
"connection.hostname.resolved": event.EventConnectionHostnameResolved;
"connection.hostname.resolve": event.EventConnectionHostnameResolve;
"connection.hostname.resolve.error": event.EventConnectionHostnameResolveError;
"connection.failed": event.EventConnectionFailed;
"connection.login": event.EventConnectionLogin;
"connection.connected": event.EventConnectionConnected;
"connection.voice.dropped": event.EventConnectionVoiceDropped;
"connection.voice.connect": event.EventConnectionVoiceConnect;
"connection.voice.connect.failed": event.EventConnectionVoiceConnectFailed;
"connection.voice.connect.succeeded": event.EventConnectionVoiceConnectSucceeded;
"connection.command.error": event.EventConnectionCommandError;
"reconnect.scheduled": event.EventReconnectScheduled;
"reconnect.canceled": event.EventReconnectCanceled;
"reconnect.execute": event.EventReconnectExecute;
"server.welcome.message": event.EventWelcomeMessage;
"server.host.message": event.EventWelcomeMessage;
"server.host.message.disconnect": event.EventHostMessageDisconnect;
"server.closed": event.EventServerClosed;
"server.requires.password": event.EventServerRequiresPassword;
"server.banned": event.EventServerBanned;
"client.view.enter": event.EventClientEnter;
"client.view.move": event.EventClientMove;
"client.view.leave": event.EventClientLeave;
"client.view.enter.own.channel": event.EventClientEnter;
"client.view.move.own.channel": event.EventClientMove;
"client.view.leave.own.channel": event.EventClientLeave;
"client.view.move.own": event.EventClientMove;
"client.nickname.change.failed": event.EventClientNicknameChangeFailed,
"client.nickname.changed": event.EventClientNicknameChanged,
"client.nickname.changed.own": event.EventClientNicknameChanged,
"channel.create": event.EventChannelCreate,
"channel.show": event.EventChannelToggle,
"channel.hide": event.EventChannelToggle,
"channel.delete": event.EventChannelDelete,
"client.poke.received": event.EventClientPokeReceived,
"client.poke.send": event.EventClientPokeSend,
"private.message.received": event.EventPrivateMessageReceived,
"private.message.send": event.EventPrivateMessageSend,
"webrtc.fatal.error": event.EventWebrtcFatalError
"disconnected": any;
}
export interface EventDispatcher<EventType extends keyof TypeInfo> {
log(data: TypeInfo[EventType], logger: ServerEventLog) : React.ReactNode;
notify(data: TypeInfo[EventType], logger: ServerEventLog);
sound(data: TypeInfo[EventType], logger: ServerEventLog);
}
export interface ServerLogUIEvents {
"query_log": {},
"notify_log": {
log: LogMessage[]
},
"notify_log_add": {
event: LogMessage
},
"notify_show": {}
}

View File

@ -1,8 +1,8 @@
import {EventType} from "../../../ui/frames/log/Definitions"; import {settings, Settings} from "tc-shared/settings";
import {Settings, settings} from "../../../settings"; import {EventType, TypeInfo} from "tc-shared/connectionlog/Definitions";
const focusDefaultStatus = {}; const focusDefaultStatus: {[T in keyof TypeInfo]?: boolean} = {};
focusDefaultStatus[EventType.CLIENT_POKE_RECEIVED] = true; focusDefaultStatus["client.poke.received"] = true;
export function requestWindowFocus() { export function requestWindowFocus() {
if(__build.target === "web") { if(__build.target === "web") {

View File

@ -1,29 +1,27 @@
import * as loader from "tc-loader"; import * as loader from "tc-loader";
import {Stage} from "tc-loader"; import {Stage} from "tc-loader";
import * as log from "../../../log";
import {LogCategory} from "../../../log";
import {EventClient, EventServerAddress, EventType, TypeInfo} from "../../../ui/frames/log/Definitions";
import {renderBBCodeAsText} from "../../../text/bbcode";
import {format_time} from "../../../ui/frames/chat";
import {ViewReasonId} from "../../../ConnectionHandler";
import {findLogDispatcher} from "../../../ui/frames/log/DispatcherLog";
import {formatDate} from "../../../MessageFormatter";
import {Settings, settings} from "../../../settings";
import {server_connections} from "tc-shared/ConnectionManager"; import {server_connections} from "tc-shared/ConnectionManager";
import {getIconManager} from "tc-shared/file/Icons"; import {getIconManager} from "tc-shared/file/Icons";
import { tra, tr } from "tc-shared/i18n/localize"; import { tra, tr } from "tc-shared/i18n/localize";
import {EventClient, EventServerAddress, EventType, TypeInfo} from "tc-shared/connectionlog/Definitions";
import {Settings, settings} from "tc-shared/settings";
import {format_time} from "tc-shared/ui/frames/chat";
import {ViewReasonId} from "tc-shared/ConnectionHandler";
import {formatDate} from "tc-shared/MessageFormatter";
import {renderBBCodeAsText} from "tc-shared/text/bbcode";
import {LogCategory, logInfo} from "tc-shared/log";
export type DispatcherLog<T extends keyof TypeInfo> = (data: TypeInfo[T], handlerId: string, eventType: T) => void; export type DispatcherLog<T extends keyof TypeInfo> = (data: TypeInfo[T], handlerId: string, eventType: T) => void;
const notificationDefaultStatus = {}; const notificationDefaultStatus: {[T in keyof TypeInfo]?: boolean} = {};
notificationDefaultStatus[EventType.CLIENT_POKE_RECEIVED] = true; notificationDefaultStatus["client.poke.received"] = true;
notificationDefaultStatus[EventType.SERVER_BANNED] = true; notificationDefaultStatus["server.banned"] = true;
notificationDefaultStatus[EventType.SERVER_CLOSED] = true; notificationDefaultStatus["server.closed"] = true;
notificationDefaultStatus[EventType.SERVER_HOST_MESSAGE_DISCONNECT] = true; notificationDefaultStatus["server.host.message.disconnect"] = true;
notificationDefaultStatus[EventType.GLOBAL_MESSAGE] = true; notificationDefaultStatus["global.message"] = true;
notificationDefaultStatus[EventType.CONNECTION_FAILED] = true; notificationDefaultStatus["connection.failed"] = true;
notificationDefaultStatus[EventType.PRIVATE_MESSAGE_RECEIVED] = true; notificationDefaultStatus["private.message.received"] = true;
notificationDefaultStatus[EventType.CONNECTION_VOICE_DROPPED] = true; notificationDefaultStatus["connection.voice.dropped"] = true;
let windowFocused = false; let windowFocused = false;
@ -210,7 +208,7 @@ registerDispatcher(EventType.SERVER_BANNED, (data, handlerId) => {
spawnServerNotification(handlerId, { spawnServerNotification(handlerId, {
body: data.invoker.client_id > 0 ? tra("You've been banned from the server by {0} for {1}.{2}", data.invoker.client_name, time, reason) : body: data.invoker.client_id > 0 ? tra("You've been banned from the server by {0} for {1}.{2}", data.invoker.client_name, time, reason) :
tra("You've been banned from the server for {0}.{1}", time, reason) tra("You've been banned from the server for {0}.{1}", time, reason)
}); });
}); });
@ -320,7 +318,7 @@ registerDispatcher(EventType.CLIENT_VIEW_MOVE, (data, handlerId) => {
}); });
}); });
registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL, findLogDispatcher(EventType.CLIENT_VIEW_MOVE)); registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL, findNotificationDispatcher(EventType.CLIENT_VIEW_MOVE));
registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN, (data, handlerId) => { registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN, (data, handlerId) => {
let message; let message;
@ -406,7 +404,7 @@ registerDispatcher(EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL, (data, handlerId) =>
break; break;
default: default:
return findLogDispatcher(EventType.CLIENT_VIEW_LEAVE)(data, handlerId, EventType.CLIENT_VIEW_LEAVE); return findNotificationDispatcher("client.view.leave")(data, handlerId, EventType.CLIENT_VIEW_LEAVE);
} }
spawnClientNotification(handlerId, data.client, { spawnClientNotification(handlerId, data.client, {
@ -482,19 +480,19 @@ registerDispatcher(EventType.PRIVATE_MESSAGE_RECEIVED, (data, handlerId) => {
}); });
registerDispatcher(EventType.WEBRTC_FATAL_ERROR, (data, handlerId) => { registerDispatcher(EventType.WEBRTC_FATAL_ERROR, (data, handlerId) => {
if(data.retryTimeout) { if(data.retryTimeout) {
let time = Math.ceil(data.retryTimeout / 1000); let time = Math.ceil(data.retryTimeout / 1000);
let minutes = Math.floor(time / 60); let minutes = Math.floor(time / 60);
let seconds = time % 60; let seconds = time % 60;
spawnServerNotification(handlerId, { spawnServerNotification(handlerId, {
body: tra("WebRTC connection closed due to a fatal error:\n{}\nRetry scheduled in {}.", data.message, (minutes > 0 ? minutes + "m" : "") + seconds + "s") body: tra("WebRTC connection closed due to a fatal error:\n{}\nRetry scheduled in {}.", data.message, (minutes > 0 ? minutes + "m" : "") + seconds + "s")
}); });
} else { } else {
spawnServerNotification(handlerId, { spawnServerNotification(handlerId, {
body: tra("WebRTC connection closed due to a fatal error:\n{}\nNo retry scheduled.", data.message) body: tra("WebRTC connection closed due to a fatal error:\n{}\nNo retry scheduled.", data.message)
}); });
} }
}); });
/* snipped PRIVATE_MESSAGE_SEND */ /* snipped PRIVATE_MESSAGE_SEND */
@ -509,14 +507,14 @@ loader.register_task(Stage.LOADED, {
/* yeahr fuck safari */ /* yeahr fuck safari */
const promise = Notification.requestPermission(result => { const promise = Notification.requestPermission(result => {
log.info(LogCategory.GENERAL, tr("Notification permission request (callback) resulted in %s"), result); logInfo(LogCategory.GENERAL, tr("Notification permission request (callback) resulted in %s"), result);
}) })
if(typeof promise !== "undefined" && 'then' in promise) { if(typeof promise !== "undefined" && 'then' in promise) {
promise.then(result => { promise.then(result => {
log.info(LogCategory.GENERAL, tr("Notification permission request resulted in %s"), result); logInfo(LogCategory.GENERAL, tr("Notification permission request resulted in %s"), result);
}).catch(error => { }).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to execute notification permission request: %O"), error); logInfo(LogCategory.GENERAL, tr("Failed to execute notification permission request: %O"), error);
}); });
} }
}, },

View File

@ -0,0 +1,61 @@
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import * as React from "react";
import {Registry} from "tc-shared/events";
import {Settings, settings} from "tc-shared/settings";
import {LogMessage, TypeInfo} from "tc-shared/connectionlog/Definitions";
import {findNotificationDispatcher, isNotificationEnabled} from "tc-shared/connectionlog/DispatcherNotifications";
import {isFocusRequestEnabled, requestWindowFocus} from "tc-shared/connectionlog/DispatcherFocus";
let uniqueLogEventId = 0;
export interface ServerEventLogEvents {
notify_log_add: { event: LogMessage }
}
export class ServerEventLog {
readonly events: Registry<ServerEventLogEvents>;
private readonly connection: ConnectionHandler;
private maxHistoryLength: number = 100;
private eventLog: LogMessage[] = [];
constructor(connection: ConnectionHandler) {
this.connection = connection;
this.events = new Registry<ServerEventLogEvents>();
}
log<T extends keyof TypeInfo>(type: T, data: TypeInfo[T]) {
const event = {
data: data,
timestamp: Date.now(),
type: type as any,
uniqueId: "log-" + Date.now() + "-" + (++uniqueLogEventId)
};
if(settings.global(Settings.FN_EVENTS_LOG_ENABLED(type), true)) {
this.eventLog.push(event);
while(this.eventLog.length > this.maxHistoryLength) {
this.eventLog.pop_front();
}
this.events.fire("notify_log_add", { event: event });
}
if(isNotificationEnabled(type as any)) {
const notification = findNotificationDispatcher(type);
if(notification) notification(data, this.connection.handlerId, type);
}
if(isFocusRequestEnabled(type as any)) {
requestWindowFocus();
}
}
getHistory() : LogMessage[] {
return this.eventLog;
}
destroy() {
this.events.destroy();
this.eventLog = undefined;
}
}

View File

@ -59,7 +59,6 @@ export abstract class AbstractChat<Events extends AbstractConversationEvents> {
protected errorMessage: string; protected errorMessage: string;
private conversationMode: ChannelConversationMode; private conversationMode: ChannelConversationMode;
protected crossChannelChatSupported: boolean = true;
protected unreadTimestamp: number; protected unreadTimestamp: number;
protected unreadState: boolean = false; protected unreadState: boolean = false;
@ -338,6 +337,9 @@ export interface AbstractChatManagerEvents<ConversationType> {
}, },
notify_unread_count_changed: { notify_unread_count_changed: {
unreadConversations: number unreadConversations: number
},
notify_cross_conversation_support_changed: {
crossConversationSupported: boolean
} }
} }
@ -351,18 +353,23 @@ export abstract class AbstractChatManager<ManagerEvents extends AbstractChatMana
private selectedConversation: ConversationType; private selectedConversation: ConversationType;
private currentUnreadCount: number; private currentUnreadCount: number;
private crossConversationSupport: boolean;
/* FIXME: Access modifier */ /* FIXME: Access modifier */
public historyUiStates: {[id: string]: { public historyUiStates: {[id: string]: {
executingUIHistoryQuery: boolean, executingUIHistoryQuery: boolean,
historyErrorMessage: string | undefined, historyErrorMessage: string | undefined,
historyRetryTimestamp: number historyRetryTimestamp: number
}} = {}; }} = {};
protected constructor(connection: ConnectionHandler) { protected constructor(connection: ConnectionHandler) {
this.connection = connection;
this.events = new Registry<ManagerEvents>(); this.events = new Registry<ManagerEvents>();
this.listenerConnection = []; this.listenerConnection = [];
this.currentUnreadCount = 0; this.currentUnreadCount = 0;
this.crossConversationSupport = true;
this.listenerUnreadTimestamp = () => { this.listenerUnreadTimestamp = () => {
let count = this.getConversations().filter(conversation => conversation.isUnread()).length; let count = this.getConversations().filter(conversation => conversation.isUnread()).length;
if(count === this.currentUnreadCount) { return; } if(count === this.currentUnreadCount) { return; }
@ -387,6 +394,10 @@ export abstract class AbstractChatManager<ManagerEvents extends AbstractChatMana
return this.currentUnreadCount; return this.currentUnreadCount;
} }
hasCrossConversationSupport() : boolean {
return this.crossConversationSupport;
}
getSelectedConversation() : ConversationType { getSelectedConversation() : ConversationType {
return this.selectedConversation; return this.selectedConversation;
} }
@ -432,4 +443,13 @@ export abstract class AbstractChatManager<ManagerEvents extends AbstractChatMana
this.listenerUnreadTimestamp(); this.listenerUnreadTimestamp();
} }
protected setCrossConversationSupport(supported: boolean) {
if(this.crossConversationSupport === supported) {
return;
}
this.crossConversationSupport = supported;
this.events.fire("notify_cross_conversation_support_changed", { crossConversationSupported: supported });
}
} }

View File

@ -16,6 +16,7 @@ import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
import {LocalClientEntry} from "tc-shared/tree/Client"; import {LocalClientEntry} from "tc-shared/tree/Client";
import {ServerCommand} from "tc-shared/connection/ConnectionBase"; import {ServerCommand} from "tc-shared/connection/ConnectionBase";
import {ChannelConversationMode} from "tc-shared/tree/Channel"; import {ChannelConversationMode} from "tc-shared/tree/Channel";
import {ServerFeature} from "tc-shared/connection/ServerFeatures";
export interface ChannelConversationEvents extends AbstractConversationEvents { export interface ChannelConversationEvents extends AbstractConversationEvents {
notify_messages_deleted: { messages: string[] }, notify_messages_deleted: { messages: string[] },
@ -45,7 +46,7 @@ export class ChannelConversation extends AbstractChat<ChannelConversationEvents>
this.setUnreadTimestamp(unreadTimestamp); this.setUnreadTimestamp(unreadTimestamp);
this.preventUnreadUpdate = false; this.preventUnreadUpdate = false;
this.events.on(["notify_unread_state_changed", "notify_read_state_changed"], event => { this.events.on(["notify_unread_state_changed", "notify_read_state_changed"], () => {
this.handle.connection.channelTree.findChannel(this.conversationId)?.setUnread(this.isReadable() && this.isUnread()); this.handle.connection.channelTree.findChannel(this.conversationId)?.setUnread(this.isReadable() && this.isUnread());
}); });
} }
@ -175,7 +176,6 @@ export class ChannelConversation extends AbstractChat<ChannelConversationEvents>
break; break;
case "unsupported": case "unsupported":
this.crossChannelChatSupported = false;
this.setConversationMode(ChannelConversationMode.Private, false); this.setConversationMode(ChannelConversationMode.Private, false);
this.setCurrentMode("normal"); this.setCurrentMode("normal");
break; break;
@ -348,6 +348,16 @@ export class ChannelConversationManager extends AbstractChatManager<ChannelConve
conversation.destroy(); conversation.destroy();
}); });
} }
if(event.newState === ConnectionState.CONNECTED) {
connection.serverFeatures.awaitFeatures().then(success => {
if(!success) { return; }
this.setCrossConversationSupport(connection.serverFeatures.supportsFeature(ServerFeature.ADVANCED_CHANNEL_CHAT));
});
} else {
this.setCrossConversationSupport(true);
}
})); }));
this.listenerConnection.push(connection.channelTree.events.on("notify_channel_updated", event => { this.listenerConnection.push(connection.channelTree.events.on("notify_channel_updated", event => {

View File

@ -5,6 +5,7 @@ import {parse as parseBBCode} from "vendor/xbbcode/parser";
import {fixupJQueryUrlTags} from "tc-shared/text/bbcode/url"; import {fixupJQueryUrlTags} from "tc-shared/text/bbcode/url";
import {fixupJQueryImageTags} from "tc-shared/text/bbcode/image"; import {fixupJQueryImageTags} from "tc-shared/text/bbcode/image";
import "./bbcode.scss"; import "./bbcode.scss";
import {BBCodeHandlerContext} from "vendor/xbbcode/renderer/react";
export const escapeBBCode = (text: string) => text.replace(/(\[)/g, "\\$1"); export const escapeBBCode = (text: string) => text.replace(/(\[)/g, "\\$1");
@ -80,11 +81,23 @@ function preprocessMessage(message: string, settings: BBCodeRenderOptions) : str
return message; return message;
} }
export const BBCodeRenderer = (props: { message: string, settings: BBCodeRenderOptions }) => ( export const BBCodeRenderer = (props: { message: string, settings: BBCodeRenderOptions, handlerId?: string }) => {
<XBBCodeRenderer options={{ tag_whitelist: allowedBBCodes }} renderer={rendererReact}> if(props.handlerId) {
{preprocessMessage(props.message, props.settings)} return (
</XBBCodeRenderer> <BBCodeHandlerContext.Provider value={props.handlerId} key={"handler-id"}>
); <XBBCodeRenderer options={{ tag_whitelist: allowedBBCodes }} renderer={rendererReact}>
{preprocessMessage(props.message, props.settings)}
</XBBCodeRenderer>
</BBCodeHandlerContext.Provider>
);
} else {
return (
<XBBCodeRenderer options={{ tag_whitelist: allowedBBCodes }} renderer={rendererReact} key={"no-handler-id"}>
{preprocessMessage(props.message, props.settings)}
</XBBCodeRenderer>
);
}
}
export function renderBBCodeAsJQuery(message: string, settings: BBCodeRenderOptions) : JQuery[] { export function renderBBCodeAsJQuery(message: string, settings: BBCodeRenderOptions) : JQuery[] {

View File

@ -8,4 +8,6 @@ export const rendererHTML = new HTMLRenderer(rendererReact);
import "./emoji"; import "./emoji";
import "./highlight"; import "./highlight";
import "./youtube"; import "./youtube";
import "./url";
import "./image";

View File

@ -4,8 +4,10 @@ import * as loader from "tc-loader";
import {ElementRenderer} from "vendor/xbbcode/renderer/base"; import {ElementRenderer} from "vendor/xbbcode/renderer/base";
import {TagElement} from "vendor/xbbcode/elements"; import {TagElement} from "vendor/xbbcode/elements";
import * as React from "react"; import * as React from "react";
import ReactRenderer from "vendor/xbbcode/renderer/react"; import ReactRenderer, {BBCodeHandlerContext} from "vendor/xbbcode/renderer/react";
import {rendererReact, rendererText} from "tc-shared/text/bbcode/renderer"; import {rendererReact, rendererText} from "tc-shared/text/bbcode/renderer";
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
import {useContext} from "react";
function spawnUrlContextMenu(pageX: number, pageY: number, target: string) { function spawnUrlContextMenu(pageX: number, pageY: number, target: string) {
contextmenu.spawn_context_menu(pageX, pageY, { contextmenu.spawn_context_menu(pageX, pageY, {
@ -31,6 +33,8 @@ function spawnUrlContextMenu(pageX: number, pageY: number, target: string) {
}); });
} }
const ClientUrlRegex = /client:\/\/([0-9]+)\/([-A-Za-z0-9+/=]+)~/g;
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "XBBCode code tag init", name: "XBBCode code tag init",
function: async () => { function: async () => {
@ -39,17 +43,36 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
const regexUrl = /^(?:[a-zA-Z]{1,16}):(?:\/{1,3}|\\)[-a-zA-Z0-9:;,@#%&()~_?+=\/\\.]*$/g; const regexUrl = /^(?:[a-zA-Z]{1,16}):(?:\/{1,3}|\\)[-a-zA-Z0-9:;,@#%&()~_?+=\/\\.]*$/g;
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> { rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
render(element: TagElement, renderer: ReactRenderer): React.ReactNode { render(element: TagElement, renderer: ReactRenderer): React.ReactNode {
let target; let target: string;
if (!element.options) if (!element.options) {
target = rendererText.render(element); target = rendererText.render(element);
else } else {
target = element.options; target = element.options;
}
regexUrl.lastIndex = 0; regexUrl.lastIndex = 0;
if (!regexUrl.test(target)) if (!regexUrl.test(target)) {
target = '#'; target = '#';
}
const handlerId = useContext(BBCodeHandlerContext);
if(handlerId) {
/* TS3-Protocol for a client */
if(target.match(ClientUrlRegex)) {
const clientData = target.match(ClientUrlRegex);
const clientDatabaseId = parseInt(clientData[1]);
const clientUniqueId = clientDatabaseId[2];
return <ClientTag
clientName={rendererText.renderContent(element).join("")}
clientUniqueId={clientUniqueId}
clientDatabaseId={clientDatabaseId > 0 ? clientDatabaseId : undefined}
handlerId={handlerId}
/>;
}
}
/* TODO: Implement client URLs */
return <a key={"er-" + ++reactId} className={"xbbcode xbbcode-tag-url"} href={target} target={"_blank"} onContextMenu={event => { return <a key={"er-" + ++reactId} className={"xbbcode xbbcode-tag-url"} href={target} target={"_blank"} onContextMenu={event => {
event.preventDefault(); event.preventDefault();
spawnUrlContextMenu(event.pageX, event.pageY, target); spawnUrlContextMenu(event.pageX, event.pageY, target);

View File

@ -4,7 +4,6 @@ import {renderMarkdownAsBBCode} from "../text/markdown";
import {escapeBBCode} from "../text/bbcode"; import {escapeBBCode} from "../text/bbcode";
import {parse as parseBBCode} from "vendor/xbbcode/parser"; import {parse as parseBBCode} from "vendor/xbbcode/parser";
import {TagElement} from "vendor/xbbcode/elements"; import {TagElement} from "vendor/xbbcode/elements";
import * as React from "react";
import {regexImage} from "tc-shared/text/bbcode/image"; import {regexImage} from "tc-shared/text/bbcode/image";
interface UrlKnifeUrl { interface UrlKnifeUrl {

View File

@ -18,10 +18,10 @@ import {formatMessage} from "../ui/frames/chat";
import {Registry} from "../events"; import {Registry} from "../events";
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry"; import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry";
import {spawnFileTransferModal} from "../ui/modal/transfer/ModalFileTransfer"; import {spawnFileTransferModal} from "../ui/modal/transfer/ModalFileTransfer";
import {EventChannelData} from "../ui/frames/log/Definitions";
import {ErrorCode} from "../connection/ErrorCode"; import {ErrorCode} from "../connection/ErrorCode";
import {ClientIcon} from "svg-sprites/client-icons"; import {ClientIcon} from "svg-sprites/client-icons";
import { tr } from "tc-shared/i18n/localize"; import { tr } from "tc-shared/i18n/localize";
import {EventChannelData} from "tc-shared/connectionlog/Definitions";
export enum ChannelType { export enum ChannelType {
PERMANENT, PERMANENT,
@ -45,7 +45,16 @@ export enum ChannelSubscribeMode {
export enum ChannelConversationMode { export enum ChannelConversationMode {
Public = 0, Public = 0,
Private = 1, Private = 1,
None = 2 None = 2,
}
export enum ChannelSidebarMode {
Conversation = 0,
Description = 1,
FileTransfer = 2,
/* Only used within client side */
Unknown = 0xFF
} }
export class ChannelProperties { export class ChannelProperties {
@ -79,8 +88,10 @@ export class ChannelProperties {
//Only after request //Only after request
channel_description: string = ""; channel_description: string = "";
channel_conversation_mode: ChannelConversationMode = 0; channel_conversation_mode: ChannelConversationMode = ChannelConversationMode.Public;
channel_conversation_history_length: number = -1; channel_conversation_history_length: number = -1;
channel_sidebar_mode: ChannelSidebarMode = ChannelSidebarMode.Unknown;
} }
export interface ChannelEvents extends ChannelTreeEntryEvents { export interface ChannelEvents extends ChannelTreeEntryEvents {
@ -99,7 +110,8 @@ export interface ChannelEvents extends ChannelTreeEntryEvents {
}, },
notify_collapsed_state_changed: { notify_collapsed_state_changed: {
collapsed: boolean collapsed: boolean
} },
notify_description_changed: {}
} }
export class ParsedChannelName { export class ParsedChannelName {
@ -173,10 +185,9 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
private _destroyed = false; private _destroyed = false;
private cachedPasswordHash: string; private cachedPasswordHash: string;
private _cached_channel_description: string = undefined; private channelDescriptionCached: boolean;
private _cached_channel_description_promise: Promise<string> = undefined; private channelDescriptionCallback: ((success: boolean) => void)[];
private _cached_channel_description_promise_resolve: any = undefined; private channelDescriptionPromise: Promise<string>;
private _cached_channel_description_promise_reject: any = undefined;
private collapsed: boolean; private collapsed: boolean;
private subscribed: boolean; private subscribed: boolean;
@ -212,18 +223,20 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
this.collapsed = this.channelTree.client.settings.server(Settings.FN_SERVER_CHANNEL_COLLAPSED(this.channelId)); this.collapsed = this.channelTree.client.settings.server(Settings.FN_SERVER_CHANNEL_COLLAPSED(this.channelId));
this.subscriptionMode = this.channelTree.client.settings.server(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this.channelId)); this.subscriptionMode = this.channelTree.client.settings.server(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this.channelId));
this.channelDescriptionCached = false;
this.channelDescriptionCallback = [];
} }
destroy() { destroy() {
this._destroyed = true; this._destroyed = true;
this.channelDescriptionCallback.forEach(callback => callback(false));
this.channelDescriptionCallback = [];
this.client_list.forEach(e => this.unregisterClient(e, true)); this.client_list.forEach(e => this.unregisterClient(e, true));
this.client_list = []; this.client_list = [];
this._cached_channel_description_promise = undefined;
this._cached_channel_description_promise_resolve = undefined;
this._cached_channel_description_promise_reject = undefined;
this.channel_previous = undefined; this.channel_previous = undefined;
this.parent = undefined; this.parent = undefined;
this.channel_next = undefined; this.channel_next = undefined;
@ -248,18 +261,39 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
return this.parsed_channel_name.text; return this.parsed_channel_name.text;
} }
getChannelDescription() : Promise<string> { async getChannelDescription() : Promise<string> {
if(this._cached_channel_description) return new Promise<string>(resolve => resolve(this._cached_channel_description)); if(this.channelDescriptionPromise) {
if(this._cached_channel_description_promise) return this._cached_channel_description_promise; return this.channelDescriptionPromise;
}
this.channelTree.client.serverConnection.send_command("channelgetdescription", {cid: this.channelId}).catch(error => { const promise = this.doGetChannelDescription();
this._cached_channel_description_promise_reject(error); this.channelDescriptionPromise = promise;
}); promise
.then(() => this.channelDescriptionPromise = undefined)
.catch(() => this.channelDescriptionPromise = undefined);
return promise;
}
return this._cached_channel_description_promise = new Promise<string>((resolve, reject) => { private async doGetChannelDescription() {
this._cached_channel_description_promise_resolve = resolve; if(!this.channelDescriptionCached) {
this._cached_channel_description_promise_reject = reject; await this.channelTree.client.serverConnection.send_command("channelgetdescription", {
}); cid: this.channelId
});
if(!this.channelDescriptionCached) {
/* since the channel description is a low command it will not be processed in sync */
await new Promise((resolve, reject) => {
this.channelDescriptionCallback.push(succeeded => {
if(succeeded) {
resolve();
} else {
reject(tr("failed to receive description"));
}
})
});
}
}
return this.properties.channel_description;
} }
registerClient(client: ClientEntry) { registerClient(client: ClientEntry) {
@ -411,7 +445,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
callback: () => { callback: () => {
const conversation = this.channelTree.client.getChannelConversations().findOrCreateConversation(this.getChannelId()); const conversation = this.channelTree.client.getChannelConversations().findOrCreateConversation(this.getChannelId());
this.channelTree.client.getChannelConversations().setSelectedConversation(conversation); this.channelTree.client.getChannelConversations().setSelectedConversation(conversation);
this.channelTree.client.getSideBar().showChannelConversations(); this.channelTree.client.getSideBar().showChannel();
}, },
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT) visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)
}, { }, {
@ -575,29 +609,27 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
let key = variable.key; let key = variable.key;
let value = variable.value; let value = variable.value;
if(!JSON.map_field_to(this.properties, value, variable.key)) { const hasUpdate = JSON.map_field_to(this.properties, value, variable.key);
/* no update */
continue; if(key == "channel_description") {
this.channelDescriptionCached = true;
this.channelDescriptionCallback.forEach(callback => callback(true));
this.channelDescriptionCallback = [];
} }
if(key == "channel_name") { if(hasUpdate) {
this.parsed_channel_name = new ParsedChannelName(value, this.hasParent()); if(key == "channel_name") {
} else if(key == "channel_order") { this.parsed_channel_name = new ParsedChannelName(value, this.hasParent());
let order = this.channelTree.findChannel(this.properties.channel_order); } else if(key == "channel_order") {
this.channelTree.moveChannel(this, order, this.parent, false); let order = this.channelTree.findChannel(this.properties.channel_order);
} else if(key === "channel_icon_id") { this.channelTree.moveChannel(this, order, this.parent, false);
this.properties.channel_icon_id = variable.value as any >>> 0; /* unsigned 32 bit number! */ } else if(key === "channel_icon_id") {
} else if(key == "channel_description") { this.properties.channel_icon_id = variable.value as any >>> 0; /* unsigned 32 bit number! */
this._cached_channel_description = undefined; } else if(key === "channel_flag_conversation_private") {
if(this._cached_channel_description_promise_resolve) /* "fix" for older TeaSpeak server versions (pre. 1.4.22) */
this._cached_channel_description_promise_resolve(value); this.properties.channel_conversation_mode = value === "1" ? 0 : 1;
this._cached_channel_description_promise = undefined; variables.push({ key: "channel_conversation_mode", value: this.properties.channel_conversation_mode + "" });
this._cached_channel_description_promise_resolve = undefined; }
this._cached_channel_description_promise_reject = undefined;
} else if(key === "channel_flag_conversation_private") {
/* "fix" for older TeaSpeak server versions (pre. 1.4.22) */
this.properties.channel_conversation_mode = value === "1" ? 0 : 1;
variables.push({ key: "channel_conversation_mode", value: this.properties.channel_conversation_mode + "" });
} }
} }
/* devel-block(log-channel-property-updates) */ /* devel-block(log-channel-property-updates) */
@ -791,4 +823,14 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
return subscribed ? ClientIcon.ChannelGreenSubscribed : ClientIcon.ChannelGreen; return subscribed ? ClientIcon.ChannelGreenSubscribed : ClientIcon.ChannelGreen;
} }
} }
handleDescriptionChanged() {
if(!this.channelDescriptionCached) {
return;
}
this.channelDescriptionCached = false;
this.properties.channel_description = undefined;
this.events.fire("notify_description_changed");
}
} }

View File

@ -115,6 +115,12 @@ export class ChannelTree {
this.tagContainer = $.spawn("div").addClass("channel-tree-container"); this.tagContainer = $.spawn("div").addClass("channel-tree-container");
renderChannelTree(this, this.tagContainer[0], { popoutButton: true }); renderChannelTree(this, this.tagContainer[0], { popoutButton: true });
this.events.on("notify_channel_list_received", () => {
if(!this.selectedEntry) {
this.setSelectedEntry(this.client.getClient().currentChannel());
}
});
this.reset(); this.reset();
} }
@ -177,13 +183,13 @@ export class ChannelTree {
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) { if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
const conversation = this.client.getChannelConversations().findOrCreateConversation(this.selectedEntry.channelId); const conversation = this.client.getChannelConversations().findOrCreateConversation(this.selectedEntry.channelId);
this.client.getChannelConversations().setSelectedConversation(conversation); this.client.getChannelConversations().setSelectedConversation(conversation);
this.client.getSideBar().showChannelConversations(); this.client.getSideBar().showChannel();
} }
} else if(this.selectedEntry instanceof ServerEntry) { } else if(this.selectedEntry instanceof ServerEntry) {
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) { if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
const conversation = this.client.getChannelConversations().findOrCreateConversation(0); const conversation = this.client.getChannelConversations().findOrCreateConversation(0);
this.client.getChannelConversations().setSelectedConversation(conversation); this.client.getChannelConversations().setSelectedConversation(conversation);
this.client.getSideBar().showChannelConversations() this.client.getSideBar().showChannel()
} }
} }
} }
@ -559,6 +565,24 @@ export class ChannelTree {
}, {width: 400, maxLength: 512}).open(); }, {width: 400, maxLength: 512}).open();
} }
}); });
client_menu.push({
type: contextmenu.MenuEntryType.ENTRY,
icon_class: ClientIcon.ChangeNickname,
name: tr("Send private message"),
callback: () => {
createInputModal(tr("Send private message"), tr("Message:<br>"), text => !!text, result => {
if (typeof(result) === "string") {
for (const client of clients) {
this.client.serverConnection.send_command("sendtextmessage", {
target: client.clientId(),
msg: result,
targetmode: 1
});
}
}
}, {width: 400, maxLength: 1024 * 8}).open();
}
});
} }
client_menu.push({ client_menu.push({
type: contextmenu.MenuEntryType.ENTRY, type: contextmenu.MenuEntryType.ENTRY,

View File

@ -22,7 +22,6 @@ import * as hex from "../crypto/hex";
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry"; import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry";
import {spawnClientVolumeChange, spawnMusicBotVolumeChange} from "../ui/modal/ModalChangeVolumeNew"; import {spawnClientVolumeChange, spawnMusicBotVolumeChange} from "../ui/modal/ModalChangeVolumeNew";
import {spawnPermissionEditorModal} from "../ui/modal/permission/ModalPermissionEditor"; import {spawnPermissionEditorModal} from "../ui/modal/permission/ModalPermissionEditor";
import {EventClient, EventType} from "../ui/frames/log/Definitions";
import {W2GPluginCmdHandler} from "../video-viewer/W2GPlugin"; import {W2GPluginCmdHandler} from "../video-viewer/W2GPlugin";
import {global_client_actions} from "../events/GlobalEvents"; import {global_client_actions} from "../events/GlobalEvents";
import {ClientIcon} from "svg-sprites/client-icons"; import {ClientIcon} from "svg-sprites/client-icons";
@ -31,6 +30,7 @@ import {VoicePlayerEvents, VoicePlayerState} from "../voice/VoicePlayer";
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions"; import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
import {VideoClient} from "tc-shared/connection/VideoConnection"; import {VideoClient} from "tc-shared/connection/VideoConnection";
import { tr } from "tc-shared/i18n/localize"; import { tr } from "tc-shared/i18n/localize";
import {EventClient} from "tc-shared/connectionlog/Definitions";
export enum ClientType { export enum ClientType {
CLIENT_VOICE, CLIENT_VOICE,
@ -573,7 +573,7 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
clid: this.clientId(), clid: this.clientId(),
msg: result msg: result
}).then(() => { }).then(() => {
this.channelTree.client.log.log(EventType.CLIENT_POKE_SEND, { this.channelTree.client.log.log("client.poke.send", {
target: this.log_data(), target: this.log_data(),
message: result message: result
}); });
@ -771,7 +771,7 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
if(variable.key == "client_nickname") { if(variable.key == "client_nickname") {
if(variable.value !== old_value && typeof(old_value) === "string") { if(variable.value !== old_value && typeof(old_value) === "string") {
if(!(this instanceof LocalClientEntry)) { /* own changes will be logged somewhere else */ if(!(this instanceof LocalClientEntry)) { /* own changes will be logged somewhere else */
this.channelTree.client.log.log(EventType.CLIENT_NICKNAME_CHANGED, { this.channelTree.client.log.log("client.nickname.changed", {
client: this.log_data(), client: this.log_data(),
new_name: variable.value, new_name: variable.value,
old_name: old_value old_name: old_value
@ -996,7 +996,7 @@ export class LocalClientEntry extends ClientEntry {
this.updateVariables({ key: "client_nickname", value: new_name }); /* change it locally */ this.updateVariables({ key: "client_nickname", value: new_name }); /* change it locally */
return this.handle.serverConnection.send_command("clientupdate", { client_nickname: new_name }).then(() => { return this.handle.serverConnection.send_command("clientupdate", { client_nickname: new_name }).then(() => {
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, new_name); settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, new_name);
this.channelTree.client.log.log(EventType.CLIENT_NICKNAME_CHANGED_OWN, { this.channelTree.client.log.log("client.nickname.changed.own", {
client: this.log_data(), client: this.log_data(),
old_name: old_name, old_name: old_name,
new_name: new_name, new_name: new_name,
@ -1004,7 +1004,7 @@ export class LocalClientEntry extends ClientEntry {
return true; return true;
}).catch((e: CommandResult) => { }).catch((e: CommandResult) => {
this.updateVariables({ key: "client_nickname", value: old_name }); /* change it back */ this.updateVariables({ key: "client_nickname", value: old_name }); /* change it back */
this.channelTree.client.log.log(EventType.CLIENT_NICKNAME_CHANGE_FAILED, { this.channelTree.client.log.log("client.nickname.change.failed", {
reason: e.extra_message reason: e.extra_message
}); });
return false; return false;

View File

@ -193,7 +193,7 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
name: tr("Join server text channel"), name: tr("Join server text channel"),
callback: () => { callback: () => {
this.channelTree.client.getChannelConversations().setSelectedConversation(this.channelTree.client.getChannelConversations().findOrCreateConversation(0)); this.channelTree.client.getChannelConversations().setSelectedConversation(this.channelTree.client.getChannelConversations().findOrCreateConversation(0));
this.channelTree.client.getSideBar().showChannelConversations(); this.channelTree.client.getSideBar().showChannel();
}, },
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT) visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)
}, { }, {

View File

@ -9,6 +9,7 @@ import * as React from "react";
import {SideBarEvents, SideBarType} from "tc-shared/ui/frames/SideBarDefinitions"; import {SideBarEvents, SideBarType} from "tc-shared/ui/frames/SideBarDefinitions";
import {Registry} from "tc-shared/events"; import {Registry} from "tc-shared/events";
import {LogCategory, logWarn} from "tc-shared/log"; import {LogCategory, logWarn} from "tc-shared/log";
import {ChannelBarController} from "tc-shared/ui/frames/side/ChannelBarController";
export class SideBarController { export class SideBarController {
private readonly uiEvents: Registry<SideBarEvents>; private readonly uiEvents: Registry<SideBarEvents>;
@ -18,8 +19,8 @@ export class SideBarController {
private header: SideHeaderController; private header: SideHeaderController;
private clientInfo: ClientInfoController; private clientInfo: ClientInfoController;
private channelConversations: ChannelConversationController;
private privateConversations: PrivateConversationController; private privateConversations: PrivateConversationController;
private channelBar: ChannelBarController;
constructor() { constructor() {
this.listenerConnection = []; this.listenerConnection = [];
@ -28,8 +29,8 @@ export class SideBarController {
this.uiEvents.on("query_content", () => this.sendContent()); this.uiEvents.on("query_content", () => this.sendContent());
this.uiEvents.on("query_content_data", event => this.sendContentData(event.content)); this.uiEvents.on("query_content_data", event => this.sendContentData(event.content));
this.channelBar = new ChannelBarController();
this.privateConversations = new PrivateConversationController(); this.privateConversations = new PrivateConversationController();
this.channelConversations = new ChannelConversationController();
this.clientInfo = new ClientInfoController(); this.clientInfo = new ClientInfoController();
this.header = new SideHeaderController(); this.header = new SideHeaderController();
} }
@ -45,8 +46,8 @@ export class SideBarController {
this.currentConnection = connection; this.currentConnection = connection;
this.header.setConnectionHandler(connection); this.header.setConnectionHandler(connection);
this.clientInfo.setConnectionHandler(connection); this.clientInfo.setConnectionHandler(connection);
this.channelConversations.setConnectionHandler(connection);
this.privateConversations.setConnectionHandler(connection); this.privateConversations.setConnectionHandler(connection);
this.channelBar.setConnectionHandler(connection);
if(connection) { if(connection) {
this.listenerConnection.push(connection.getSideBar().events.on("notify_content_type_changed", () => this.sendContent())); this.listenerConnection.push(connection.getSideBar().events.on("notify_content_type_changed", () => this.sendContent()));
@ -59,14 +60,14 @@ export class SideBarController {
this.header?.destroy(); this.header?.destroy();
this.header = undefined; this.header = undefined;
this.channelBar?.destroy();
this.channelBar = undefined;
this.clientInfo?.destroy(); this.clientInfo?.destroy();
this.clientInfo = undefined; this.clientInfo = undefined;
this.privateConversations?.destroy(); this.privateConversations?.destroy();
this.privateConversations = undefined; this.privateConversations = undefined;
this.channelConversations?.destroy();
this.channelConversations = undefined;
} }
renderInto(container: HTMLDivElement) { renderInto(container: HTMLDivElement) {
@ -93,17 +94,16 @@ export class SideBarController {
}); });
break; break;
case "channel-chat": case "channel":
if(!this.currentConnection) { if(!this.currentConnection) {
logWarn(LogCategory.GENERAL, tr("Received channel chat content data request without an active connection.")); logWarn(LogCategory.GENERAL, tr("Received channel chat content data request without an active connection."));
return; return;
} }
this.uiEvents.fire_react("notify_content_data", { this.uiEvents.fire_react("notify_content_data", {
content: "channel-chat", content: "channel",
data: { data: {
events: this.channelConversations["uiEvents"], events: this.channelBar.uiEvents,
handlerId: this.currentConnection.handlerId
} }
}); });
break; break;

View File

@ -1,17 +1,16 @@
import {Registry} from "tc-shared/events"; import {Registry} from "tc-shared/events";
import {PrivateConversationUIEvents} from "tc-shared/ui/frames/side/PrivateConversationDefinitions"; import {PrivateConversationUIEvents} from "tc-shared/ui/frames/side/PrivateConversationDefinitions";
import {AbstractConversationUiEvents} from "./side/AbstractConversationDefinitions";
import {ClientInfoEvents} from "tc-shared/ui/frames/side/ClientInfoDefinitions"; import {ClientInfoEvents} from "tc-shared/ui/frames/side/ClientInfoDefinitions";
import {SideHeaderEvents} from "tc-shared/ui/frames/side/HeaderDefinitions"; import {SideHeaderEvents} from "tc-shared/ui/frames/side/HeaderDefinitions";
import {ChannelBarUiEvents} from "tc-shared/ui/frames/side/ChannelBarDefinitions";
/* TODO: Somehow outsource the event registries to IPC? */ /* TODO: Somehow outsource the event registries to IPC? */
export type SideBarType = "none" | "channel-chat" | "private-chat" | "client-info" | "music-manage"; export type SideBarType = "none" | "channel" | "private-chat" | "client-info" | "music-manage";
export interface SideBarTypeData { export interface SideBarTypeData {
"none": {}, "none": {},
"channel-chat": { "channel": {
events: Registry<AbstractConversationUiEvents>, events: Registry<ChannelBarUiEvents>
handlerId: string
}, },
"private-chat": { "private-chat": {
events: Registry<PrivateConversationUIEvents>, events: Registry<PrivateConversationUIEvents>,

View File

@ -1,12 +1,13 @@
import {SideHeaderEvents, SideHeaderState} from "tc-shared/ui/frames/side/HeaderDefinitions"; import {SideHeaderEvents, SideHeaderState} from "tc-shared/ui/frames/side/HeaderDefinitions";
import {Registry} from "tc-shared/events"; import {Registry} from "tc-shared/events";
import React = require("react");
import {SideHeaderRenderer} from "tc-shared/ui/frames/side/HeaderRenderer"; import {SideHeaderRenderer} from "tc-shared/ui/frames/side/HeaderRenderer";
import {ConversationPanel} from "tc-shared/ui/frames/side/AbstractConversationRenderer";
import {SideBarEvents, SideBarType, SideBarTypeData} from "tc-shared/ui/frames/SideBarDefinitions"; import {SideBarEvents, SideBarType, SideBarTypeData} from "tc-shared/ui/frames/SideBarDefinitions";
import {useContext, useState} from "react"; import {useContext, useState} from "react";
import {ClientInfoRenderer} from "tc-shared/ui/frames/side/ClientInfoRenderer"; import {ClientInfoRenderer} from "tc-shared/ui/frames/side/ClientInfoRenderer";
import {PrivateConversationsPanel} from "tc-shared/ui/frames/side/PrivateConversationRenderer"; import {PrivateConversationsPanel} from "tc-shared/ui/frames/side/PrivateConversationRenderer";
import {ChannelBarRenderer} from "tc-shared/ui/frames/side/ChannelBarRenderer";
import {LogCategory, logWarn} from "tc-shared/log";
import React = require("react");
const cssStyle = require("./SideBarRenderer.scss"); const cssStyle = require("./SideBarRenderer.scss");
@ -23,17 +24,14 @@ function useContentData<T extends SideBarType>(type: T) : SideBarTypeData[T] {
return contentData; return contentData;
} }
const ContentRendererChannelConversation = () => { const ContentRendererChannel = () => {
const contentData = useContentData("channel-chat"); const contentData = useContentData("channel");
if(!contentData) { return null; } if(!contentData) { return null; }
return ( return (
<ConversationPanel <ChannelBarRenderer
key={"channel-chat"} key={"channel"}
events={contentData.events} events={contentData.events}
handlerId={contentData.handlerId}
messagesDeletable={true}
noFirstMessageOverlay={false}
/> />
); );
}; };
@ -63,8 +61,8 @@ const ContentRendererClientInfo = () => {
const SideBarFrame = (props: { type: SideBarType }) => { const SideBarFrame = (props: { type: SideBarType }) => {
switch (props.type) { switch (props.type) {
case "channel-chat": case "channel":
return <ContentRendererChannelConversation key={props.type} />; return <ContentRendererChannel key={props.type} />;
case "private-chat": case "private-chat":
return <ContentRendererPrivateConversation key={props.type} />; return <ContentRendererPrivateConversation key={props.type} />;
@ -88,7 +86,7 @@ const SideBarHeader = (props: { type: SideBarType, eventsHeader: Registry<SideHe
headerState = { state: "none" }; headerState = { state: "none" };
break; break;
case "channel-chat": case "channel":
headerState = { state: "conversation", mode: "channel" }; headerState = { state: "conversation", mode: "channel" };
break; break;
@ -103,6 +101,11 @@ const SideBarHeader = (props: { type: SideBarType, eventsHeader: Registry<SideHe
case "music-manage": case "music-manage":
headerState = { state: "music-bot" }; headerState = { state: "music-bot" };
break; break;
default:
logWarn(LogCategory.GENERAL, tr("Side bar header with invalid type: %s"), props.type);
headerState = { state: "none" };
break;
} }
return <SideHeaderRenderer state={headerState} events={props.eventsHeader} />; return <SideHeaderRenderer state={headerState} events={props.eventsHeader} />;

View File

@ -0,0 +1,49 @@
import {Registry} from "tc-shared/events";
import {ServerEventLogUiEvents} from "tc-shared/ui/frames/log/Definitions";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
export class ServerEventLogController {
readonly events: Registry<ServerEventLogUiEvents>;
private currentConnection: ConnectionHandler;
private listenerConnection: (() => void)[];
constructor() {
this.events = new Registry<ServerEventLogUiEvents>();
this.events.on("query_handler_id", () => this.events.fire_react("notify_handler_id", { handlerId: this.currentConnection?.handlerId }));
this.events.on("query_log", () => this.sendLogs());
}
destroy() {
this.listenerConnection?.forEach(callback => callback());
this.listenerConnection = [];
this.events.destroy();
}
setConnectionHandler(handler: ConnectionHandler) {
if(this.currentConnection === handler) {
return;
}
this.listenerConnection?.forEach(callback => callback());
this.listenerConnection = [];
this.currentConnection = handler;
if(this.currentConnection) {
this.listenerConnection.push(this.currentConnection.log.events.on("notify_log_add", event => {
this.events.fire_react("notify_log_add", { event: event.event });
}));
}
this.events.fire_react("notify_handler_id", { handlerId: handler?.handlerId });
}
private sendLogs() {
const logs = this.currentConnection?.log.getHistory() || [];
this.events.fire_react("notify_log", { events: logs });
}
}

View File

@ -1,348 +1,16 @@
import {PermissionInfo} from "../../../permission/PermissionManager"; import {LogMessage} from "tc-shared/connectionlog/Definitions";
import {ViewReasonId} from "../../../ConnectionHandler";
import * as React from "react";
import {ServerEventLog} from "../../../ui/frames/log/ServerEventLog";
/* FIXME: Remove this! */ export interface ServerEventLogUiEvents {
export enum EventType { query_handler_id: {},
CONNECTION_BEGIN = "connection.begin", query_log: {},
CONNECTION_HOSTNAME_RESOLVE = "connection.hostname.resolve",
CONNECTION_HOSTNAME_RESOLVE_ERROR = "connection.hostname.resolve.error",
CONNECTION_HOSTNAME_RESOLVED = "connection.hostname.resolved",
CONNECTION_LOGIN = "connection.login",
CONNECTION_CONNECTED = "connection.connected",
CONNECTION_FAILED = "connection.failed",
DISCONNECTED = "disconnected", notify_log_add: {
CONNECTION_VOICE_CONNECT = "connection.voice.connect",
CONNECTION_VOICE_CONNECT_FAILED = "connection.voice.connect.failed",
CONNECTION_VOICE_CONNECT_SUCCEEDED = "connection.voice.connect.succeeded",
CONNECTION_VOICE_DROPPED = "connection.voice.dropped",
CONNECTION_COMMAND_ERROR = "connection.command.error",
GLOBAL_MESSAGE = "global.message",
SERVER_WELCOME_MESSAGE = "server.welcome.message",
SERVER_HOST_MESSAGE = "server.host.message",
SERVER_HOST_MESSAGE_DISCONNECT = "server.host.message.disconnect",
SERVER_CLOSED = "server.closed",
SERVER_BANNED = "server.banned",
SERVER_REQUIRES_PASSWORD = "server.requires.password",
CLIENT_VIEW_ENTER = "client.view.enter",
CLIENT_VIEW_LEAVE = "client.view.leave",
CLIENT_VIEW_MOVE = "client.view.move",
CLIENT_VIEW_ENTER_OWN_CHANNEL = "client.view.enter.own.channel",
CLIENT_VIEW_LEAVE_OWN_CHANNEL = "client.view.leave.own.channel",
CLIENT_VIEW_MOVE_OWN_CHANNEL = "client.view.move.own.channel",
CLIENT_VIEW_MOVE_OWN = "client.view.move.own",
CLIENT_NICKNAME_CHANGED = "client.nickname.changed",
CLIENT_NICKNAME_CHANGED_OWN = "client.nickname.changed.own",
CLIENT_NICKNAME_CHANGE_FAILED = "client.nickname.change.failed",
CLIENT_SERVER_GROUP_ADD = "client.server.group.add",
CLIENT_SERVER_GROUP_REMOVE = "client.server.group.remove",
CLIENT_CHANNEL_GROUP_CHANGE = "client.channel.group.change",
PRIVATE_MESSAGE_RECEIVED = "private.message.received",
PRIVATE_MESSAGE_SEND = "private.message.send",
CHANNEL_CREATE = "channel.create",
CHANNEL_DELETE = "channel.delete",
ERROR_CUSTOM = "error.custom",
ERROR_PERMISSION = "error.permission",
CLIENT_POKE_RECEIVED = "client.poke.received",
CLIENT_POKE_SEND = "client.poke.send",
RECONNECT_SCHEDULED = "reconnect.scheduled",
RECONNECT_EXECUTE = "reconnect.execute",
RECONNECT_CANCELED = "reconnect.canceled",
WEBRTC_FATAL_ERROR = "webrtc.fatal.error"
}
export type EventClient = {
client_unique_id: string;
client_name: string;
client_id: number;
}
export type EventChannelData = {
channel_id: number;
channel_name: string;
}
export type EventServerData = {
server_name: string;
server_unique_id: string;
}
export type EventServerAddress = {
server_hostname: string;
server_port: number;
}
export namespace event {
export type EventGlobalMessage = {
isOwnMessage: boolean;
sender: EventClient;
message: string;
}
export type EventConnectBegin = {
address: EventServerAddress;
client_nickname: string;
}
export type EventErrorCustom = {
message: string;
}
export type EventReconnectScheduled = {
timeout: number;
}
export type EventReconnectCanceled = { }
export type EventReconnectExecute = { }
export type EventErrorPermission = {
permission: PermissionInfo;
}
export type EventWelcomeMessage = {
message: string;
}
export type EventHostMessageDisconnect = {
message: string;
}
export type EventClientMove = {
channel_from?: EventChannelData;
channel_from_own: boolean;
channel_to?: EventChannelData;
channel_to_own: boolean;
client: EventClient;
client_own: boolean;
invoker?: EventClient;
message?: string;
reason: ViewReasonId;
}
export type EventClientEnter = {
channel_from?: EventChannelData;
channel_to?: EventChannelData;
client: EventClient;
invoker?: EventClient;
message?: string;
reason: ViewReasonId;
ban_time?: number;
}
export type EventClientLeave = {
channel_from?: EventChannelData;
channel_to?: EventChannelData;
client: EventClient;
invoker?: EventClient;
message?: string;
reason: ViewReasonId;
ban_time?: number;
}
export type EventChannelCreate = {
creator: EventClient,
channel: EventChannelData,
ownAction: boolean
}
export type EventChannelToggle = {
channel: EventChannelData
}
export type EventChannelDelete = {
deleter: EventClient,
channel: EventChannelData,
ownAction: boolean
}
export type EventConnectionConnected = {
serverName: string,
serverAddress: EventServerAddress,
own_client: EventClient;
}
export type EventConnectionFailed = {
serverAddress: EventServerAddress
}
export type EventConnectionLogin = {}
export type EventConnectionHostnameResolve = {};
export type EventConnectionHostnameResolved = {
address: EventServerAddress;
}
export type EventConnectionHostnameResolveError = {
message: string;
}
export type EventConnectionVoiceConnectFailed = {
reason: string;
reconnect_delay: number; /* if less or equal to 0 reconnect is prohibited */
}
export type EventConnectionVoiceConnectSucceeded = {}
export type EventConnectionVoiceConnect = {
attemptCount: number
}
export type EventConnectionVoiceDropped = {}
export type EventConnectionCommandError = {
error: any;
}
export type EventClientNicknameChanged = {
client: EventClient;
old_name: string;
new_name: string;
}
export type EventClientNicknameChangeFailed = {
reason: string;
}
export type EventServerClosed = {
message: string;
}
export type EventServerRequiresPassword = {}
export type EventServerBanned = {
message: string;
time: number;
invoker: EventClient;
}
export type EventClientPokeReceived = {
sender: EventClient,
message: string
}
export type EventClientPokeSend = {
target: EventClient,
message: string
}
export type EventPrivateMessageSend = {
target: EventClient,
message: string
}
export type EventPrivateMessageReceived = {
sender: EventClient,
message: string
}
export type EventWebrtcFatalError = {
message: string,
retryTimeout: number | 0
}
}
export type LogMessage = {
type: EventType;
uniqueId: string;
timestamp: number;
data: any;
}
export interface TypeInfo {
"connection.begin" : event.EventConnectBegin;
"global.message": event.EventGlobalMessage;
"error.custom": event.EventErrorCustom;
"error.permission": event.EventErrorPermission;
"connection.hostname.resolved": event.EventConnectionHostnameResolved;
"connection.hostname.resolve": event.EventConnectionHostnameResolve;
"connection.hostname.resolve.error": event.EventConnectionHostnameResolveError;
"connection.failed": event.EventConnectionFailed;
"connection.login": event.EventConnectionLogin;
"connection.connected": event.EventConnectionConnected;
"connection.voice.dropped": event.EventConnectionVoiceDropped;
"connection.voice.connect": event.EventConnectionVoiceConnect;
"connection.voice.connect.failed": event.EventConnectionVoiceConnectFailed;
"connection.voice.connect.succeeded": event.EventConnectionVoiceConnectSucceeded;
"connection.command.error": event.EventConnectionCommandError;
"reconnect.scheduled": event.EventReconnectScheduled;
"reconnect.canceled": event.EventReconnectCanceled;
"reconnect.execute": event.EventReconnectExecute;
"server.welcome.message": event.EventWelcomeMessage;
"server.host.message": event.EventWelcomeMessage;
"server.host.message.disconnect": event.EventHostMessageDisconnect;
"server.closed": event.EventServerClosed;
"server.requires.password": event.EventServerRequiresPassword;
"server.banned": event.EventServerBanned;
"client.view.enter": event.EventClientEnter;
"client.view.move": event.EventClientMove;
"client.view.leave": event.EventClientLeave;
"client.view.enter.own.channel": event.EventClientEnter;
"client.view.move.own.channel": event.EventClientMove;
"client.view.leave.own.channel": event.EventClientLeave;
"client.view.move.own": event.EventClientMove;
"client.nickname.change.failed": event.EventClientNicknameChangeFailed,
"client.nickname.changed": event.EventClientNicknameChanged,
"client.nickname.changed.own": event.EventClientNicknameChanged,
"channel.create": event.EventChannelCreate,
"channel.show": event.EventChannelToggle,
"channel.hide": event.EventChannelToggle,
"channel.delete": event.EventChannelDelete,
"client.poke.received": event.EventClientPokeReceived,
"client.poke.send": event.EventClientPokeSend,
"private.message.received": event.EventPrivateMessageReceived,
"private.message.send": event.EventPrivateMessageSend,
"webrtc.fatal.error": event.EventWebrtcFatalError
"disconnected": any;
}
export interface EventDispatcher<EventType extends keyof TypeInfo> {
log(data: TypeInfo[EventType], logger: ServerEventLog) : React.ReactNode;
notify(data: TypeInfo[EventType], logger: ServerEventLog);
sound(data: TypeInfo[EventType], logger: ServerEventLog);
}
export interface ServerLogUIEvents {
"query_log": {},
"notify_log": {
log: LogMessage[]
},
"notify_log_add": {
event: LogMessage event: LogMessage
}, },
"notify_show": {} notify_log: {
events: LogMessage[]
},
notify_handler_id: {
handlerId: string | undefined
}
} }

View File

@ -17,7 +17,7 @@
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
min-height: 2em; min-height: 1em;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;

View File

@ -1,12 +1,17 @@
import {LogMessage, ServerLogUIEvents} from "tc-shared/ui/frames/log/Definitions";
import {VariadicTranslatable} from "tc-shared/ui/react-elements/i18n"; import {VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
import {Registry} from "tc-shared/events"; import {Registry} from "tc-shared/events";
import {useEffect, useRef, useState} from "react"; import {useContext, useEffect, useRef, useState} from "react";
import * as React from "react"; import * as React from "react";
import {findLogDispatcher} from "tc-shared/ui/frames/log/DispatcherLog"; import {findLogEventRenderer} from "./RendererEvent";
import {LogMessage} from "tc-shared/connectionlog/Definitions";
import {ServerEventLogUiEvents} from "tc-shared/ui/frames/log/Definitions";
import {useDependentState} from "tc-shared/ui/react-elements/Helper";
const cssStyle = require("./Renderer.scss"); const cssStyle = require("./Renderer.scss");
const HandlerIdContext = React.createContext<string>(undefined);
const EventsContext = React.createContext<Registry<ServerEventLogUiEvents>>(undefined);
const LogFallbackDispatcher = (_unused, __unused, eventType) => ( const LogFallbackDispatcher = (_unused, __unused, eventType) => (
<div className={cssStyle.errorMessage}> <div className={cssStyle.errorMessage}>
<VariadicTranslatable text={"Missing log entry builder for {0}"}> <VariadicTranslatable text={"Missing log entry builder for {0}"}>
@ -15,12 +20,14 @@ const LogFallbackDispatcher = (_unused, __unused, eventType) => (
</div> </div>
); );
const LogEntryRenderer = React.memo((props: { entry: LogMessage, handlerId: string }) => { const LogEntryRenderer = React.memo((props: { entry: LogMessage }) => {
const dispatcher = findLogDispatcher(props.entry.type as any) || LogFallbackDispatcher; const handlerId = useContext(HandlerIdContext);
const rendered = dispatcher(props.entry.data, props.handlerId, props.entry.type); const dispatcher = findLogEventRenderer(props.entry.type as any) || LogFallbackDispatcher;
const rendered = dispatcher(props.entry.data, handlerId, props.entry.type);
if(!rendered) /* hide message */ if(!rendered) { /* hide message */
return null; return null;
}
const date = new Date(props.entry.timestamp); const date = new Date(props.entry.timestamp);
return ( return (
@ -35,25 +42,27 @@ const LogEntryRenderer = React.memo((props: { entry: LogMessage, handlerId: stri
); );
}); });
export const ServerLogRenderer = (props: { events: Registry<ServerLogUIEvents>, handlerId: string }) => { const ServerLogRenderer = () => {
const [ logs, setLogs ] = useState<LogMessage[] | "loading">(() => { const handlerId = useContext(HandlerIdContext);
props.events.fire_react("query_log"); const events = useContext(EventsContext);
const [ logs, setLogs ] = useDependentState<LogMessage[] | "loading">(() => {
events.fire_react("query_log");
return "loading"; return "loading";
}); }, [ handlerId ]);
const [ revision, setRevision ] = useState(0); const [ revision, setRevision ] = useState(0);
const refContainer = useRef<HTMLDivElement>(); const refContainer = useRef<HTMLDivElement>();
const scrollOffset = useRef<number | "bottom">("bottom"); const scrollOffset = useRef<number | "bottom">("bottom");
props.events.reactUse("notify_log", event => { events.reactUse("notify_log", event => {
const logs = event.log.slice(0); const logs = event.events.slice(0);
logs.splice(0, Math.max(0, logs.length - 100)); logs.splice(0, Math.max(0, logs.length - 100));
logs.sort((a, b) => a.timestamp - b.timestamp); logs.sort((a, b) => a.timestamp - b.timestamp);
setLogs(logs); setLogs(logs);
}); });
props.events.reactUse("notify_log_add", event => { events.reactUse("notify_log_add", event => {
if(logs === "loading") { if(logs === "loading") {
return; return;
} }
@ -72,10 +81,6 @@ export const ServerLogRenderer = (props: { events: Registry<ServerLogUIEvents>,
refContainer.current.scrollTop = scrollOffset.current === "bottom" ? refContainer.current.scrollHeight : scrollOffset.current; refContainer.current.scrollTop = scrollOffset.current === "bottom" ? refContainer.current.scrollHeight : scrollOffset.current;
}; };
props.events.reactUse("notify_show", () => {
requestAnimationFrame(fixScroll);
});
useEffect(() => { useEffect(() => {
const id = requestAnimationFrame(fixScroll); const id = requestAnimationFrame(fixScroll);
return () => cancelAnimationFrame(id); return () => cancelAnimationFrame(id);
@ -91,7 +96,23 @@ export const ServerLogRenderer = (props: { events: Registry<ServerLogUIEvents>,
scrollOffset.current = shouldFollow ? "bottom" : top; scrollOffset.current = shouldFollow ? "bottom" : top;
}}> }}>
{logs === "loading" ? null : logs.map(e => <LogEntryRenderer key={e.uniqueId} entry={e} handlerId={props.handlerId} />)} {logs === "loading" ? null : logs.map(e => <LogEntryRenderer key={e.uniqueId} entry={e} />)}
</div> </div>
); );
}; };
export const ServerLogFrame = (props: { events: Registry<ServerEventLogUiEvents> }) => {
const [ handlerId, setHandlerId ] = useState<string>(() => {
props.events.fire("query_handler_id");
return undefined;
});
props.events.reactUse("notify_handler_id", event => setHandlerId(event.handlerId));
return (
<EventsContext.Provider value={props.events}>
<HandlerIdContext.Provider value={handlerId}>
<ServerLogRenderer />
</HandlerIdContext.Provider>
</EventsContext.Provider>
);
}

View File

@ -1,5 +1,4 @@
import {ViewReasonId} from "tc-shared/ConnectionHandler"; import {ViewReasonId} from "tc-shared/ConnectionHandler";
import {EventChannelData, EventClient, EventType, TypeInfo} from "tc-shared/ui/frames/log/Definitions";
import * as React from "react"; import * as React from "react";
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n"; import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
import {formatDate} from "tc-shared/MessageFormatter"; import {formatDate} from "tc-shared/MessageFormatter";
@ -8,22 +7,23 @@ import {format_time} from "tc-shared/ui/frames/chat";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {XBBCodeRenderer} from "vendor/xbbcode/react"; import {XBBCodeRenderer} from "vendor/xbbcode/react";
import {ChannelTag, ClientTag} from "tc-shared/ui/tree/EntryTags"; import {ChannelTag, ClientTag} from "tc-shared/ui/tree/EntryTags";
import {EventChannelData, EventClient, EventType, TypeInfo} from "tc-shared/connectionlog/Definitions";
const cssStyle = require("./DispatcherLog.scss"); const cssStyle = require("./DispatcherLog.scss");
const cssStyleRenderer = require("./Renderer.scss"); const cssStyleRenderer = require("./Renderer.scss");
export type DispatcherLog<T extends keyof TypeInfo> = (data: TypeInfo[T], handlerId: string, eventType: T) => React.ReactNode; export type RendererEvent<T extends keyof TypeInfo> = (data: TypeInfo[T], handlerId: string, eventType: T) => React.ReactNode;
const dispatchers: {[key: string]: DispatcherLog<any>} = { }; const dispatchers: {[T in keyof TypeInfo]?: RendererEvent<T>} = { };
function registerDispatcher<T extends keyof TypeInfo>(key: T, builder: DispatcherLog<T>) { function registerRenderer<T extends keyof TypeInfo>(key: T, builder: RendererEvent<T>) {
dispatchers[key] = builder; dispatchers[key] = builder as any;
} }
export function findLogDispatcher<T extends keyof TypeInfo>(type: T) : DispatcherLog<T> { export function findLogEventRenderer<T extends keyof TypeInfo>(type: T) : RendererEvent<T> {
return dispatchers[type]; return dispatchers[type] as any;
} }
export function getRegisteredLogDispatchers() : TypeInfo[] { export function getRegisteredLogEventRenderer() : TypeInfo[] {
return Object.keys(dispatchers) as any; return Object.keys(dispatchers) as any;
} }
@ -46,66 +46,66 @@ const ChannelRenderer = (props: { channel: EventChannelData, handlerId: string,
/> />
); );
registerDispatcher(EventType.ERROR_CUSTOM, data => <div className={cssStyleRenderer.errorMessage}>{data.message}</div>); registerRenderer(EventType.ERROR_CUSTOM, data => <div className={cssStyleRenderer.errorMessage}>{data.message}</div>);
registerDispatcher(EventType.CONNECTION_BEGIN, data => ( registerRenderer(EventType.CONNECTION_BEGIN, data => (
<VariadicTranslatable text={"Connecting to {0}{1}"}> <VariadicTranslatable text={"Connecting to {0}{1}"}>
<>{data.address.server_hostname}</> <>{data.address.server_hostname}</>
<>{data.address.server_port == 9987 ? "" : (":" + data.address.server_port)}</> <>{data.address.server_port == 9987 ? "" : (":" + data.address.server_port)}</>
</VariadicTranslatable> </VariadicTranslatable>
)); ));
registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVE, () => ( registerRenderer(EventType.CONNECTION_HOSTNAME_RESOLVE, () => (
<Translatable>Resolving hostname</Translatable> <Translatable>Resolving hostname</Translatable>
)); ));
registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVED, data => ( registerRenderer(EventType.CONNECTION_HOSTNAME_RESOLVED, data => (
<VariadicTranslatable text={"Hostname resolved successfully to {0}:{1}"}> <VariadicTranslatable text={"Hostname resolved successfully to {0}:{1}"}>
<>{data.address.server_hostname}</> <>{data.address.server_hostname}</>
<>{data.address.server_port}</> <>{data.address.server_port}</>
</VariadicTranslatable> </VariadicTranslatable>
)); ));
registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVE_ERROR, data => ( registerRenderer(EventType.CONNECTION_HOSTNAME_RESOLVE_ERROR, data => (
<VariadicTranslatable text={"Failed to resolve hostname. Connecting to given hostname. Error: {0}"}> <VariadicTranslatable text={"Failed to resolve hostname. Connecting to given hostname. Error: {0}"}>
<>{data.message}</> <>{data.message}</>
</VariadicTranslatable> </VariadicTranslatable>
)); ));
registerDispatcher(EventType.CONNECTION_LOGIN, () => ( registerRenderer(EventType.CONNECTION_LOGIN, () => (
<Translatable>Logging in...</Translatable> <Translatable>Logging in...</Translatable>
)); ));
registerDispatcher(EventType.CONNECTION_FAILED, () => ( registerRenderer(EventType.CONNECTION_FAILED, () => (
<Translatable>Connect failed.</Translatable> <Translatable>Connect failed.</Translatable>
)); ));
registerDispatcher(EventType.CONNECTION_CONNECTED, (data,handlerId) => ( registerRenderer(EventType.CONNECTION_CONNECTED, (data,handlerId) => (
<VariadicTranslatable text={"Connected as {0}"}> <VariadicTranslatable text={"Connected as {0}"}>
<ClientRenderer client={data.own_client} handlerId={handlerId} /> <ClientRenderer client={data.own_client} handlerId={handlerId} />
</VariadicTranslatable> </VariadicTranslatable>
)); ));
registerDispatcher(EventType.CONNECTION_VOICE_CONNECT, () => ( registerRenderer(EventType.CONNECTION_VOICE_CONNECT, () => (
<Translatable>Connecting voice bridge.</Translatable> <Translatable>Connecting voice bridge.</Translatable>
)); ));
registerDispatcher(EventType.CONNECTION_VOICE_CONNECT_SUCCEEDED, () => ( registerRenderer(EventType.CONNECTION_VOICE_CONNECT_SUCCEEDED, () => (
<Translatable>Voice bridge successfully connected.</Translatable> <Translatable>Voice bridge successfully connected.</Translatable>
)); ));
registerDispatcher(EventType.CONNECTION_VOICE_CONNECT_FAILED, (data) => ( registerRenderer(EventType.CONNECTION_VOICE_CONNECT_FAILED, (data) => (
<VariadicTranslatable text={"Failed to setup voice bridge: {0}. Allow reconnect: {1}"}> <VariadicTranslatable text={"Failed to setup voice bridge: {0}. Allow reconnect: {1}"}>
<>{data.reason}</> <>{data.reason}</>
{data.reconnect_delay > 0 ? <Translatable>Yes</Translatable> : <Translatable>No</Translatable>} {data.reconnect_delay > 0 ? <Translatable>Yes</Translatable> : <Translatable>No</Translatable>}
</VariadicTranslatable> </VariadicTranslatable>
)); ));
registerDispatcher(EventType.CONNECTION_VOICE_DROPPED, () => ( registerRenderer(EventType.CONNECTION_VOICE_DROPPED, () => (
<Translatable>Voice bridge has been dropped. Trying to reconnect.</Translatable> <Translatable>Voice bridge has been dropped. Trying to reconnect.</Translatable>
)); ));
registerDispatcher(EventType.ERROR_PERMISSION, data => ( registerRenderer(EventType.ERROR_PERMISSION, data => (
<div className={cssStyleRenderer.errorMessage}> <div className={cssStyleRenderer.errorMessage}>
<VariadicTranslatable text={"Insufficient client permissions. Failed on permission {0}"}> <VariadicTranslatable text={"Insufficient client permissions. Failed on permission {0}"}>
<>{data.permission ? data.permission.name : <Translatable>unknown</Translatable>}</> <>{data.permission ? data.permission.name : <Translatable>unknown</Translatable>}</>
@ -113,7 +113,7 @@ registerDispatcher(EventType.ERROR_PERMISSION, data => (
</div> </div>
)); ));
registerDispatcher(EventType.CLIENT_VIEW_ENTER, (data, handlerId) => { registerRenderer(EventType.CLIENT_VIEW_ENTER, (data, handlerId) => {
switch (data.reason) { switch (data.reason) {
case ViewReasonId.VREASON_USER_ACTION: case ViewReasonId.VREASON_USER_ACTION:
if(data.channel_from) { if(data.channel_from) {
@ -189,7 +189,7 @@ registerDispatcher(EventType.CLIENT_VIEW_ENTER, (data, handlerId) => {
} }
}); });
registerDispatcher(EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL, (data, handlerId) => { registerRenderer(EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL, (data, handlerId) => {
switch (data.reason) { switch (data.reason) {
case ViewReasonId.VREASON_USER_ACTION: case ViewReasonId.VREASON_USER_ACTION:
if(data.channel_from) { if(data.channel_from) {
@ -265,7 +265,7 @@ registerDispatcher(EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL, (data, handlerId) =>
} }
}); });
registerDispatcher(EventType.CLIENT_VIEW_MOVE, (data, handlerId) => { registerRenderer(EventType.CLIENT_VIEW_MOVE, (data, handlerId) => {
switch (data.reason) { switch (data.reason) {
case ViewReasonId.VREASON_MOVED: case ViewReasonId.VREASON_MOVED:
return ( return (
@ -308,9 +308,9 @@ registerDispatcher(EventType.CLIENT_VIEW_MOVE, (data, handlerId) => {
} }
}); });
registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL, findLogDispatcher(EventType.CLIENT_VIEW_MOVE)); registerRenderer(EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL, findLogEventRenderer(EventType.CLIENT_VIEW_MOVE));
registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN, (data, handlerId) => { registerRenderer(EventType.CLIENT_VIEW_MOVE_OWN, (data, handlerId) => {
switch (data.reason) { switch (data.reason) {
case ViewReasonId.VREASON_MOVED: case ViewReasonId.VREASON_MOVED:
return ( return (
@ -353,7 +353,7 @@ registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN, (data, handlerId) => {
} }
}); });
registerDispatcher(EventType.CLIENT_VIEW_LEAVE, (data, handlerId) => { registerRenderer(EventType.CLIENT_VIEW_LEAVE, (data, handlerId) => {
switch (data.reason) { switch (data.reason) {
case ViewReasonId.VREASON_USER_ACTION: case ViewReasonId.VREASON_USER_ACTION:
return ( return (
@ -434,7 +434,7 @@ registerDispatcher(EventType.CLIENT_VIEW_LEAVE, (data, handlerId) => {
} }
}); });
registerDispatcher(EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL, (data, handlerId) => { registerRenderer(EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL, (data, handlerId) => {
switch (data.reason) { switch (data.reason) {
case ViewReasonId.VREASON_USER_ACTION: case ViewReasonId.VREASON_USER_ACTION:
return ( return (
@ -456,24 +456,24 @@ registerDispatcher(EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL, (data, handlerId) =>
); );
default: default:
return findLogDispatcher(EventType.CLIENT_VIEW_LEAVE)(data, handlerId, EventType.CLIENT_VIEW_LEAVE); return findLogEventRenderer(EventType.CLIENT_VIEW_LEAVE)(data, handlerId, EventType.CLIENT_VIEW_LEAVE);
} }
}); });
registerDispatcher(EventType.SERVER_WELCOME_MESSAGE,data => ( registerRenderer(EventType.SERVER_WELCOME_MESSAGE,(data, handlerId) => (
<BBCodeRenderer message={"[color=green]" + data.message + "[/color]"} settings={{convertSingleUrls: false}} /> <BBCodeRenderer message={"[color=green]" + data.message + "[/color]"} settings={{convertSingleUrls: false}} handlerId={handlerId} />
)); ));
registerDispatcher(EventType.SERVER_HOST_MESSAGE,data => ( registerRenderer(EventType.SERVER_HOST_MESSAGE,(data, handlerId) => (
<BBCodeRenderer message={"[color=green]" + data.message + "[/color]"} settings={{convertSingleUrls: false}} /> <BBCodeRenderer message={"[color=green]" + data.message + "[/color]"} settings={{convertSingleUrls: false}} handlerId={handlerId} />
)); ));
registerDispatcher(EventType.SERVER_HOST_MESSAGE_DISCONNECT,data => ( registerRenderer(EventType.SERVER_HOST_MESSAGE_DISCONNECT,(data, handlerId) => (
<BBCodeRenderer message={"[color=red]" + data.message + "[/color]"} settings={{convertSingleUrls: false}} /> <BBCodeRenderer message={"[color=red]" + data.message + "[/color]"} settings={{convertSingleUrls: false}} handlerId={handlerId} />
)); ));
registerDispatcher(EventType.CLIENT_NICKNAME_CHANGED,(data, handlerId) => ( registerRenderer(EventType.CLIENT_NICKNAME_CHANGED,(data, handlerId) => (
<VariadicTranslatable text={"{0} changed his nickname from \"{1}\" to \"{2}\""}> <VariadicTranslatable text={"{0} changed his nickname from \"{1}\" to \"{2}\""}>
<ClientRenderer client={data.client} handlerId={handlerId} /> <ClientRenderer client={data.client} handlerId={handlerId} />
<>{data.old_name}</> <>{data.old_name}</>
@ -481,44 +481,44 @@ registerDispatcher(EventType.CLIENT_NICKNAME_CHANGED,(data, handlerId) => (
</VariadicTranslatable> </VariadicTranslatable>
)); ));
registerDispatcher(EventType.CLIENT_NICKNAME_CHANGED_OWN,() => ( registerRenderer(EventType.CLIENT_NICKNAME_CHANGED_OWN,() => (
<Translatable>Nickname successfully changed.</Translatable> <Translatable>Nickname successfully changed.</Translatable>
)); ));
registerDispatcher(EventType.CLIENT_NICKNAME_CHANGE_FAILED,(data) => ( registerRenderer(EventType.CLIENT_NICKNAME_CHANGE_FAILED,(data) => (
<VariadicTranslatable text={"Failed to change nickname: {0}"}> <VariadicTranslatable text={"Failed to change nickname: {0}"}>
<>{data.reason}</> <>{data.reason}</>
</VariadicTranslatable> </VariadicTranslatable>
)); ));
registerDispatcher(EventType.GLOBAL_MESSAGE, (data, handlerId) => <> registerRenderer(EventType.GLOBAL_MESSAGE, (data, handlerId) => <>
<VariadicTranslatable text={"{} send a server message: {1}"}> <VariadicTranslatable text={"{} send a server message: {1}"}>
<ClientRenderer client={data.sender} handlerId={handlerId} /> <ClientRenderer client={data.sender} handlerId={handlerId} />
<XBBCodeRenderer>{data.message}</XBBCodeRenderer> <BBCodeRenderer settings={{ convertSingleUrls: false }} message={data.message} handlerId={handlerId} />
</VariadicTranslatable> </VariadicTranslatable>
</>); </>);
registerDispatcher(EventType.DISCONNECTED,() => ( registerRenderer(EventType.DISCONNECTED,() => (
<Translatable>Disconnected from server</Translatable> <Translatable>Disconnected from server</Translatable>
)); ));
registerDispatcher(EventType.RECONNECT_SCHEDULED,data => ( registerRenderer(EventType.RECONNECT_SCHEDULED,data => (
<VariadicTranslatable text={"Reconnecting in {0}."}> <VariadicTranslatable text={"Reconnecting in {0}."}>
<>{format_time(data.timeout, tr("now"))}</> <>{format_time(data.timeout, tr("now"))}</>
</VariadicTranslatable> </VariadicTranslatable>
)); ));
registerDispatcher(EventType.RECONNECT_CANCELED,() => ( registerRenderer(EventType.RECONNECT_CANCELED,() => (
<Translatable>Reconnect canceled.</Translatable> <Translatable>Reconnect canceled.</Translatable>
)); ));
registerDispatcher(EventType.RECONNECT_CANCELED,() => ( registerRenderer(EventType.RECONNECT_CANCELED,() => (
<Translatable>Reconnecting...</Translatable> <Translatable>Reconnecting...</Translatable>
)); ));
registerDispatcher(EventType.SERVER_BANNED,(data, handlerId) => { registerRenderer(EventType.SERVER_BANNED,(data, handlerId) => {
const time = data.time === 0 ? <Translatable>ever</Translatable> : <>{format_time(data.time * 1000, tr("one second"))}</>; const time = data.time === 0 ? <Translatable>ever</Translatable> : <>{format_time(data.time * 1000, tr("one second"))}</>;
const reason = data.message ? <> <Translatable>Reason:</Translatable>&nbsp;{data.message}</> : undefined; const reason = data.message ? <> <Translatable>Reason:</Translatable>&nbsp;{data.message}</> : undefined;
@ -544,11 +544,11 @@ registerDispatcher(EventType.SERVER_BANNED,(data, handlerId) => {
); );
}); });
registerDispatcher(EventType.SERVER_REQUIRES_PASSWORD,() => ( registerRenderer(EventType.SERVER_REQUIRES_PASSWORD,() => (
<Translatable>Server requires a password to connect.</Translatable> <Translatable>Server requires a password to connect.</Translatable>
)); ));
registerDispatcher(EventType.SERVER_CLOSED,data => { registerRenderer(EventType.SERVER_CLOSED,data => {
if(data.message) if(data.message)
return ( return (
<VariadicTranslatable text={"Server has been closed ({})."}> <VariadicTranslatable text={"Server has been closed ({})."}>
@ -558,7 +558,7 @@ registerDispatcher(EventType.SERVER_CLOSED,data => {
return <Translatable>Server has been closed.</Translatable>; return <Translatable>Server has been closed.</Translatable>;
}); });
registerDispatcher(EventType.CONNECTION_COMMAND_ERROR,data => { registerRenderer(EventType.CONNECTION_COMMAND_ERROR,data => {
let message; let message;
if(typeof data.error === "string") if(typeof data.error === "string")
message = data.error; message = data.error;
@ -576,7 +576,7 @@ registerDispatcher(EventType.CONNECTION_COMMAND_ERROR,data => {
) )
}); });
registerDispatcher(EventType.CHANNEL_CREATE,(data, handlerId) => { registerRenderer(EventType.CHANNEL_CREATE,(data, handlerId) => {
if(data.ownAction) { if(data.ownAction) {
return ( return (
<VariadicTranslatable text={"Channel {} has been created."}> <VariadicTranslatable text={"Channel {} has been created."}>
@ -593,13 +593,13 @@ registerDispatcher(EventType.CHANNEL_CREATE,(data, handlerId) => {
} }
}); });
registerDispatcher("channel.show",(data, handlerId) => ( registerRenderer("channel.show",(data, handlerId) => (
<VariadicTranslatable text={"Channel {} has appeared."}> <VariadicTranslatable text={"Channel {} has appeared."}>
<ChannelRenderer channel={data.channel} handlerId={handlerId} /> <ChannelRenderer channel={data.channel} handlerId={handlerId} />
</VariadicTranslatable> </VariadicTranslatable>
)); ));
registerDispatcher(EventType.CHANNEL_DELETE,(data, handlerId) => { registerRenderer(EventType.CHANNEL_DELETE,(data, handlerId) => {
if(data.ownAction) { if(data.ownAction) {
return ( return (
<VariadicTranslatable text={"Channel {} has been deleted."}> <VariadicTranslatable text={"Channel {} has been deleted."}>
@ -616,24 +616,24 @@ registerDispatcher(EventType.CHANNEL_DELETE,(data, handlerId) => {
} }
}); });
registerDispatcher("channel.hide",(data, handlerId) => ( registerRenderer("channel.hide",(data, handlerId) => (
<VariadicTranslatable text={"Channel {} has disappeared."}> <VariadicTranslatable text={"Channel {} has disappeared."}>
<ChannelRenderer channel={data.channel} handlerId={handlerId} /> <ChannelRenderer channel={data.channel} handlerId={handlerId} />
</VariadicTranslatable> </VariadicTranslatable>
)); ));
registerDispatcher(EventType.CLIENT_POKE_SEND,(data, handlerId) => ( registerRenderer(EventType.CLIENT_POKE_SEND,(data, handlerId) => (
<VariadicTranslatable text={"You poked {}."}> <VariadicTranslatable text={"You poked {}."}>
<ClientRenderer client={data.target} handlerId={handlerId} /> <ClientRenderer client={data.target} handlerId={handlerId} />
</VariadicTranslatable> </VariadicTranslatable>
)); ));
registerDispatcher(EventType.CLIENT_POKE_RECEIVED,(data, handlerId) => { registerRenderer(EventType.CLIENT_POKE_RECEIVED,(data, handlerId) => {
if(data.message) { if(data.message) {
return ( return (
<VariadicTranslatable text={"You received a poke from {}: {}"}> <VariadicTranslatable text={"You received a poke from {}: {}"}>
<ClientRenderer client={data.sender} handlerId={handlerId} /> <ClientRenderer client={data.sender} handlerId={handlerId} />
<BBCodeRenderer message={data.message} settings={{ convertSingleUrls: false }} /> <BBCodeRenderer message={data.message} settings={{ convertSingleUrls: false }} handlerId={handlerId} />
</VariadicTranslatable> </VariadicTranslatable>
); );
} else { } else {
@ -645,10 +645,10 @@ registerDispatcher(EventType.CLIENT_POKE_RECEIVED,(data, handlerId) => {
} }
}); });
registerDispatcher(EventType.PRIVATE_MESSAGE_RECEIVED, () => undefined); registerRenderer(EventType.PRIVATE_MESSAGE_RECEIVED, () => undefined);
registerDispatcher(EventType.PRIVATE_MESSAGE_SEND, () => undefined); registerRenderer(EventType.PRIVATE_MESSAGE_SEND, () => undefined);
registerDispatcher(EventType.WEBRTC_FATAL_ERROR, (data) => { registerRenderer(EventType.WEBRTC_FATAL_ERROR, (data) => {
if(data.retryTimeout) { if(data.retryTimeout) {
let time = Math.ceil(data.retryTimeout / 1000); let time = Math.ceil(data.retryTimeout / 1000);
let minutes = Math.floor(time / 60); let minutes = Math.floor(time / 60);

View File

@ -1,86 +0,0 @@
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import * as React from "react";
import {LogMessage, ServerLogUIEvents, TypeInfo} from "tc-shared/ui/frames/log/Definitions";
import {Registry} from "tc-shared/events";
import * as ReactDOM from "react-dom";
import {ServerLogRenderer} from "tc-shared/ui/frames/log/Renderer";
import {findNotificationDispatcher, isNotificationEnabled} from "tc-shared/ui/frames/log/DispatcherNotifications";
import {Settings, settings} from "tc-shared/settings";
import {isFocusRequestEnabled, requestWindowFocus} from "tc-shared/ui/frames/log/DispatcherFocus";
const cssStyle = require("./Renderer.scss");
let uniqueLogEventId = 0;
export class ServerEventLog {
private readonly connection: ConnectionHandler;
private readonly uiEvents: Registry<ServerLogUIEvents>;
private readonly listenerHandlerVisibilityChanged;
private htmlTag: HTMLDivElement;
private maxHistoryLength: number = 100;
private eventLog: LogMessage[] = [];
constructor(connection: ConnectionHandler) {
this.connection = connection;
this.uiEvents = new Registry<ServerLogUIEvents>();
this.htmlTag = document.createElement("div");
this.htmlTag.classList.add(cssStyle.htmlTag);
this.uiEvents.on("query_log", () => {
this.uiEvents.fire_react("notify_log", { log: this.eventLog.slice() });
});
ReactDOM.render(<ServerLogRenderer events={this.uiEvents} handlerId={this.connection.handlerId} />, this.htmlTag);
this.connection.events().on("notify_visibility_changed", this.listenerHandlerVisibilityChanged =event => {
if(event.visible) {
this.uiEvents.fire("notify_show");
}
});
}
log<T extends keyof TypeInfo>(type: T, data: TypeInfo[T]) {
const event = {
data: data,
timestamp: Date.now(),
type: type as any,
uniqueId: "log-" + Date.now() + "-" + (++uniqueLogEventId)
};
if(settings.global(Settings.FN_EVENTS_LOG_ENABLED(type), true)) {
this.eventLog.push(event);
while(this.eventLog.length > this.maxHistoryLength)
this.eventLog.pop_front();
this.uiEvents.fire_react("notify_log_add", { event: event });
}
if(isNotificationEnabled(type as any)) {
const notification = findNotificationDispatcher(type);
if(notification) notification(data, this.connection.handlerId, type);
}
if(isFocusRequestEnabled(type as any)) {
requestWindowFocus();
}
}
getHTMLTag() {
return this.htmlTag;
}
destroy() {
if(this.htmlTag) {
ReactDOM.unmountComponentAtNode(this.htmlTag);
this.htmlTag?.remove();
this.htmlTag = undefined;
}
this.connection.events().off(this.listenerHandlerVisibilityChanged);
this.eventLog = undefined;
this.uiEvents.destroy();
}
}

View File

@ -29,8 +29,6 @@ export abstract class AbstractConversationController<
protected currentSelectedConversation: ConversationType; protected currentSelectedConversation: ConversationType;
protected currentSelectedListener: (() => void)[]; protected currentSelectedListener: (() => void)[];
protected crossChannelChatSupported = true;
protected constructor() { protected constructor() {
this.uiEvents = new Registry<Events>(); this.uiEvents = new Registry<Events>();
this.currentSelectedListener = []; this.currentSelectedListener = [];
@ -68,6 +66,12 @@ export abstract class AbstractConversationController<
protected registerConversationManagerEvents(manager: Manager) { protected registerConversationManagerEvents(manager: Manager) {
this.listenerManager.push(manager.events.on("notify_selected_changed", event => this.setCurrentlySelected(event.newConversation))); this.listenerManager.push(manager.events.on("notify_selected_changed", event => this.setCurrentlySelected(event.newConversation)));
this.listenerManager.push(manager.events.on("notify_cross_conversation_support_changed", () => {
const currentConversation = this.getCurrentConversation();
if(currentConversation) {
this.reportStateToUI(currentConversation);
}
}));
} }
protected registerConversationEvents(conversation: ConversationType) { protected registerConversationEvents(conversation: ConversationType) {
@ -138,7 +142,7 @@ export abstract class AbstractConversationController<
this.uiEvents.fire_react("notify_conversation_state", { this.uiEvents.fire_react("notify_conversation_state", {
chatId: conversation.getChatId(), chatId: conversation.getChatId(),
state: "private", state: "private",
crossChannelChatSupported: this.crossChannelChatSupported crossChannelChatSupported: this.conversationManager.hasCrossConversationSupport()
}); });
return; return;
} }
@ -154,7 +158,7 @@ export abstract class AbstractConversationController<
chatFrameMaxMessageCount: kMaxChatFrameMessageSize, chatFrameMaxMessageCount: kMaxChatFrameMessageSize,
unreadTimestamp: conversation.getUnreadTimestamp(), unreadTimestamp: conversation.getUnreadTimestamp(),
showUserSwitchEvents: conversation.isPrivate() || !this.crossChannelChatSupported, showUserSwitchEvents: conversation.isPrivate() || !this.conversationManager.hasCrossConversationSupport(),
sendEnabled: conversation.isSendEnabled(), sendEnabled: conversation.isSendEnabled(),
events: [...conversation.getPresentEvents(), ...conversation.getPresentMessages()] events: [...conversation.getPresentEvents(), ...conversation.getPresentMessages()]
@ -251,18 +255,6 @@ export abstract class AbstractConversationController<
return this.currentSelectedConversation; return this.currentSelectedConversation;
} }
protected setCrossChannelChatSupport(flag: boolean) {
if(this.crossChannelChatSupported === flag) {
return;
}
this.crossChannelChatSupported = flag;
const currentConversation = this.getCurrentConversation();
if(currentConversation) {
this.reportStateToUI(currentConversation);
}
}
@EventHandler<AbstractConversationUiEvents>("query_conversation_state") @EventHandler<AbstractConversationUiEvents>("query_conversation_state")
protected handleQueryConversationState(event: AbstractConversationUiEvents["query_conversation_state"]) { protected handleQueryConversationState(event: AbstractConversationUiEvents["query_conversation_state"]) {
const conversation = this.conversationManager?.findConversationById(event.chatId); const conversation = this.conversationManager?.findConversationById(event.chatId);

View File

@ -56,6 +56,9 @@ html:root {
position: relative; position: relative;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
.containerMessages { .containerMessages {
flex-grow: 1; flex-grow: 1;
flex-shrink: 1; flex-shrink: 1;

View File

@ -26,9 +26,9 @@ import {ChatBox} from "tc-shared/ui/react-elements/ChatBox";
const cssStyle = require("./AbstractConversationRenderer.scss"); const cssStyle = require("./AbstractConversationRenderer.scss");
const ChatMessageTextRenderer = React.memo((props: { text: string }) => { const ChatMessageTextRenderer = React.memo((props: { text: string, handlerId: string }) => {
if(typeof props.text !== "string") { debugger; } if(typeof props.text !== "string") { debugger; }
return <BBCodeRenderer settings={{ convertSingleUrls: true }} message={props.text || ""} />; return <BBCodeRenderer settings={{ convertSingleUrls: true }} message={props.text || ""} handlerId={props.handlerId} />;
}); });
const ChatEventMessageRenderer = React.memo((props: { const ChatEventMessageRenderer = React.memo((props: {
@ -71,7 +71,7 @@ const ChatEventMessageRenderer = React.memo((props: {
<br /> { /* Only for copy purposes */ } <br /> { /* Only for copy purposes */ }
</div> </div>
<div className={cssStyle.text}> <div className={cssStyle.text}>
<ChatMessageTextRenderer text={props.message.message} /> <ChatMessageTextRenderer text={props.message.message} handlerId={props.handlerId} />
</div> </div>
<br style={{ content: " ", display: "none" }} /> { /* Only for copy purposes */ } <br style={{ content: " ", display: "none" }} /> { /* Only for copy purposes */ }
</div> </div>

View File

@ -0,0 +1,194 @@
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {Registry} from "tc-shared/events";
import {ChannelBarMode, ChannelBarUiEvents} from "tc-shared/ui/frames/side/ChannelBarDefinitions";
import {ChannelEntry, ChannelSidebarMode} from "tc-shared/tree/Channel";
import {ChannelConversationController} from "tc-shared/ui/frames/side/ChannelConversationController";
import {ChannelDescriptionController} from "tc-shared/ui/frames/side/ChannelDescriptionController";
import {LocalClientEntry} from "tc-shared/tree/Client";
export class ChannelBarController {
readonly uiEvents: Registry<ChannelBarUiEvents>;
private channelConversations: ChannelConversationController;
private description: ChannelDescriptionController;
private currentConnection: ConnectionHandler;
private listenerConnection: (() => void)[];
private currentChannel: ChannelEntry;
private listenerChannel: (() => void)[];
constructor() {
this.uiEvents = new Registry<ChannelBarUiEvents>();
this.listenerConnection = [];
this.listenerChannel = [];
this.channelConversations = new ChannelConversationController();
this.description = new ChannelDescriptionController();
this.uiEvents.on("query_mode", () => this.notifyChannelMode());
this.uiEvents.on("query_channel_id", () => this.notifyChannelId());
this.uiEvents.on("query_data", event => this.notifyModeData(event.mode));
}
destroy() {
this.listenerConnection.forEach(callback => callback());
this.listenerConnection = [];
this.listenerChannel.forEach(callback => callback());
this.listenerChannel = [];
this.currentChannel = undefined;
this.currentConnection = undefined;
this.channelConversations?.destroy();
this.channelConversations = undefined;
this.description?.destroy();
this.description = undefined;
this.uiEvents.destroy();
}
setConnectionHandler(handler: ConnectionHandler) {
if(this.currentConnection === handler) {
return;
}
this.channelConversations.setConnectionHandler(handler);
this.listenerConnection.forEach(callback => callback());
this.listenerConnection = [];
this.currentConnection = handler;
const selectedEntry = handler?.channelTree.getSelectedEntry();
if(selectedEntry instanceof ChannelEntry) {
this.setChannel(selectedEntry);
} else {
this.setChannel(undefined);
}
if(handler) {
this.listenerConnection.push(handler.channelTree.events.on("notify_selected_entry_changed", event => {
if(event.newEntry instanceof ChannelEntry) {
this.setChannel(event.newEntry);
}
}));
this.listenerConnection.push(handler.channelTree.events.on("notify_client_moved", event => {
if(event.client instanceof LocalClientEntry) {
if(event.oldChannel === this.currentChannel || event.newChannel === this.currentChannel) {
/* The mode may changed since we can now write in the channel */
this.notifyChannelMode();
}
}
}));
this.listenerConnection.push(handler.getChannelConversations().events.on("notify_cross_conversation_support_changed", () => {
this.notifyChannelMode();
}));
}
}
private setChannel(channel: ChannelEntry) {
if(this.currentChannel === channel) {
return;
}
this.description.setChannel(channel);
this.listenerChannel.forEach(callback => callback());
this.listenerChannel = [];
this.currentChannel = channel;
this.notifyChannelId();
if(channel) {
this.listenerChannel.push(channel.events.on("notify_properties_updated", event => {
if("channel_sidebar_mode" in event.updated_properties) {
this.notifyChannelMode();
}
}));
}
}
private notifyChannelId() {
this.uiEvents.fire_react("notify_channel_id", {
channelId: this.currentChannel ? this.currentChannel.channelId : -1,
handlerId: this.currentConnection ? this.currentConnection.handlerId : "unbound"
});
}
private notifyChannelMode() {
let mode: ChannelBarMode = "none";
if(this.currentChannel) {
switch(this.currentChannel.properties.channel_sidebar_mode) {
case ChannelSidebarMode.Description:
mode = "description";
break;
case ChannelSidebarMode.FileTransfer:
mode = "file-transfer";
break;
case ChannelSidebarMode.Conversation:
mode = "conversation";
break;
case ChannelSidebarMode.Unknown:
default:
if(this.currentConnection) {
const channelConversation = this.currentConnection.getChannelConversations();
if(channelConversation.hasCrossConversationSupport() || this.currentChannel === this.currentConnection.getClient().currentChannel()) {
mode = "conversation";
} else {
/* A really old TeaSpeak server or a TeamSpeak server. */
mode = "description";
}
} else {
mode = "none";
}
break;
}
}
this.uiEvents.fire_react("notify_mode", { mode: mode });
}
private notifyModeData(mode: ChannelBarMode) {
switch (mode) {
case "none":
this.uiEvents.fire_react("notify_data", { content: "none", data: {} });
break;
case "conversation":
this.uiEvents.fire_react("notify_data", {
content: "conversation",
data: {
events: this.channelConversations.getUiEvents()
}
});
break;
case "description":
this.uiEvents.fire_react("notify_data", {
content: "description",
data: {
events: this.description.uiEvents
}
});
break;
case "file-transfer":
this.uiEvents.fire_react("notify_data", {
content: "file-transfer",
data: {
}
});
/* TODO! */
break;
}
}
}

View File

@ -0,0 +1,36 @@
import {Registry} from "tc-shared/events";
import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions";
import {ChannelDescriptionUiEvents} from "tc-shared/ui/frames/side/ChannelDescriptionDefinitions";
export type ChannelBarMode = "conversation" | "description" | "file-transfer" | "none";
export interface ChannelBarModeData {
"conversation": {
events: Registry<ChannelConversationUiEvents>,
},
"description": {
events: Registry<ChannelDescriptionUiEvents>
},
"file-transfer": {
/* TODO! */
},
"none": {}
}
export type ChannelBarNotifyModeData<T extends keyof ChannelBarModeData> = {
content: T,
data: ChannelBarModeData[T]
}
export interface ChannelBarUiEvents {
query_mode: {},
query_channel_id: {},
query_data: { mode: ChannelBarMode },
notify_mode: { mode: ChannelBarMode },
notify_channel_id: {
channelId: number,
handlerId: string
},
notify_data: ChannelBarNotifyModeData<ChannelBarMode>
}

View File

@ -0,0 +1,89 @@
import {Registry} from "tc-shared/events";
import {ChannelBarMode, ChannelBarModeData, ChannelBarUiEvents} from "tc-shared/ui/frames/side/ChannelBarDefinitions";
import {useContext, useState} from "react";
import * as React from "react";
import {ConversationPanel} from "tc-shared/ui/frames/side/AbstractConversationRenderer";
import {useDependentState} from "tc-shared/ui/react-elements/Helper";
import {ChannelDescriptionRenderer} from "tc-shared/ui/frames/side/ChannelDescriptionRenderer";
const EventContext = React.createContext<Registry<ChannelBarUiEvents>>(undefined);
const ChannelContext = React.createContext<{ channelId: number, handlerId: string }>(undefined);
function useModeData<T extends ChannelBarMode>(type: T, dependencies: any[]) : ChannelBarModeData[T] {
const events = useContext(EventContext);
const [ contentData, setContentData ] = useDependentState(() => {
events.fire("query_data", { mode: type });
return undefined;
}, dependencies);
events.reactUse("notify_data", event => event.content === type && setContentData(event.data));
return contentData;
}
const ModeRenderer = () => {
const events = useContext(EventContext);
const channelContext = useContext(ChannelContext);
const [ mode, setMode ] = useDependentState<ChannelBarMode>(() => {
events.fire("query_mode");
return "none";
}, [ channelContext.channelId, channelContext.handlerId ]);
events.reactUse("notify_mode", event => setMode(event.mode));
switch (mode) {
case "conversation":
return <ModeRendererConversation key={"conversation"} />;
case "description":
return <ModeRendererDescription key={"description"} />;
case "file-transfer":
/* TODO! */
return null;
case "none":
default:
return null;
}
};
const ModeRendererConversation = React.memo(() => {
const channelContext = useContext(ChannelContext);
const data = useModeData("conversation", [ channelContext ]);
if(!data) { return null; }
return (
<ConversationPanel
key={"conversation"}
events={data.events}
handlerId={channelContext.handlerId}
messagesDeletable={true}
noFirstMessageOverlay={false}
/>
);
});
const ModeRendererDescription = React.memo(() => {
const channelContext = useContext(ChannelContext);
const data = useModeData("description", [ channelContext ]);
if(!data) { return null; }
return (
<ChannelDescriptionRenderer events={data.events} />
);
});
export const ChannelBarRenderer = (props: { events: Registry<ChannelBarUiEvents> }) => {
const [ channelContext, setChannelContext ] = useState<{ channelId: number, handlerId: string }>(() => {
props.events.fire("query_channel_id");
return { channelId: -1, handlerId: "unbound" };
});
props.events.reactUse("notify_channel_id", event => setChannelContext({ handlerId: event.handlerId, channelId: event.channelId }));
return (
<EventContext.Provider value={props.events}>
<ChannelContext.Provider value={channelContext}>
<ModeRenderer />
</ChannelContext.Provider>
</EventContext.Provider>
);
}

View File

@ -11,7 +11,6 @@ import {
ChannelConversationManager, ChannelConversationManager,
ChannelConversationManagerEvents ChannelConversationManagerEvents
} from "tc-shared/conversations/ChannelConversationManager"; } from "tc-shared/conversations/ChannelConversationManager";
import {ServerFeature} from "tc-shared/connection/ServerFeatures";
import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions"; import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions";
export class ChannelConversationController extends AbstractConversationController< export class ChannelConversationController extends AbstractConversationController<
@ -75,18 +74,6 @@ export class ChannelConversationController extends AbstractConversationControlle
this.handlePanelShow(); this.handlePanelShow();
})); }));
this.connectionListener.push(connection.events().on("notify_connection_state_changed", event => {
if(event.newState === ConnectionState.CONNECTED) {
connection.serverFeatures.awaitFeatures().then(success => {
if(!success) { return; }
this.setCrossChannelChatSupport(connection.serverFeatures.supportsFeature(ServerFeature.ADVANCED_CHANNEL_CHAT));
});
} else {
this.setCrossChannelChatSupport(true);
}
}));
} }
@EventHandler<AbstractConversationUiEvents>("action_delete_message") @EventHandler<AbstractConversationUiEvents>("action_delete_message")

View File

@ -0,0 +1,121 @@
import {ChannelEntry} from "tc-shared/tree/Channel";
import {Registry} from "tc-shared/events";
import {
ChannelDescriptionStatus,
ChannelDescriptionUiEvents
} from "tc-shared/ui/frames/side/ChannelDescriptionDefinitions";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {LogCategory, logError} from "tc-shared/log";
import {ErrorCode} from "tc-shared/connection/ErrorCode";
export class ChannelDescriptionController {
readonly uiEvents: Registry<ChannelDescriptionUiEvents>;
private currentChannel: ChannelEntry;
private listenerChannel: (() => void)[];
private descriptionSendPending = false;
private cachedDescriptionStatus: ChannelDescriptionStatus;
private cachedDescriptionAge: number;
constructor() {
this.uiEvents = new Registry<ChannelDescriptionUiEvents>();
this.listenerChannel = [];
this.uiEvents.on("query_description", () => this.notifyDescription());
this.uiEvents.enableDebug("channel-description");
this.cachedDescriptionAge = 0;
}
destroy() {
this.listenerChannel.forEach(callback => callback());
this.listenerChannel = [];
this.currentChannel = undefined;
this.uiEvents.destroy();
}
setChannel(channel: ChannelEntry) {
if(this.currentChannel === channel) {
return;
}
this.listenerChannel.forEach(callback => callback());
this.listenerChannel = [];
this.currentChannel = channel;
this.cachedDescriptionStatus = undefined;
if(channel) {
this.listenerChannel.push(channel.events.on("notify_properties_updated", event => {
if("channel_description" in event.updated_properties) {
this.notifyDescription().then(undefined);
}
}));
this.listenerChannel.push(channel.events.on("notify_description_changed", () => {
this.notifyDescription().then(undefined);
}));
}
this.notifyDescription().then(undefined);
}
private async notifyDescription() {
if(this.descriptionSendPending) {
return;
}
this.descriptionSendPending = true;
try {
if(Date.now() - this.cachedDescriptionAge > 5000 || !this.cachedDescriptionStatus) {
await this.updateCachedDescriptionStatus();
}
this.uiEvents.fire_react("notify_description", { status: this.cachedDescriptionStatus });
} finally {
this.descriptionSendPending = false;
}
}
private async updateCachedDescriptionStatus() {
try {
let description;
if(this.currentChannel) {
description = await new Promise<any>((resolve, reject) => {
this.currentChannel.getChannelDescription().then(resolve).catch(reject);
setTimeout(() => reject(tr("timeout")), 5000);
});
}
this.cachedDescriptionStatus = {
status: "success",
description: description,
handlerId: this.currentChannel.channelTree.client.handlerId
};
} catch (error) {
if(error instanceof CommandResult) {
if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
const permission = this.currentChannel?.channelTree.client.permissions.resolveInfo(parseInt(error.json["failed_permid"]));
this.cachedDescriptionStatus = {
status: "no-permissions",
failedPermission: permission ? permission.name : "unknown"
};
return;
}
error = error.formattedMessage();
} else if(typeof error !== "string") {
logError(LogCategory.GENERAL, tr("Failed to get channel descriptions: %o"), error);
error = tr("lookup the console");
}
this.cachedDescriptionStatus = {
status: "error",
reason: error
};
}
this.cachedDescriptionAge = Date.now();
}
}

View File

@ -0,0 +1,18 @@
export type ChannelDescriptionStatus = {
status: "success",
description: string,
handlerId: string
} | {
status: "error",
reason: string
} | {
status: "no-permissions",
failedPermission: string
};
export interface ChannelDescriptionUiEvents {
query_description: {},
notify_description: {
status: ChannelDescriptionStatus,
}
}

View File

@ -0,0 +1,51 @@
@import "../../../../css/static/mixin";
@import "../../../../css/static/properties";
.container {
display: flex;
flex-direction: column;
justify-content: flex-start;
height: 100%;
width: 100%;
color: #999;
&.centeredText {
justify-content: center;
color: var(--chat-overlay);
}
&.error {
color: #ac5353;
}
code {
padding: 0;
}
}
.centeredText .text {
align-self: center;
text-align: center;
}
.descriptionContainer {
padding: .5em;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
color: #999;
overflow: auto;
@include chat-scrollbar();
font-size: 12px;
img {
max-width: 100%;
}
}

View File

@ -0,0 +1,86 @@
import {
ChannelDescriptionStatus,
ChannelDescriptionUiEvents
} from "tc-shared/ui/frames/side/ChannelDescriptionDefinitions";
import {Registry} from "tc-shared/events";
import * as React from "react";
import {useState} from "react";
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
import {allowedBBCodes, BBCodeRenderer} from "tc-shared/text/bbcode";
import {parse as parseBBCode} from "vendor/xbbcode/parser";
const cssStyle = require("./ChannelDescriptionRenderer.scss");
const CenteredTextRenderer = (props: { children?: React.ReactElement | (React.ReactElement | string)[], className?: string }) => {
return (
<div className={cssStyle.container + " " + cssStyle.centeredText + " " + props.className}>
<div className={cssStyle.text}>
{props.children}
</div>
</div>
);
}
const DescriptionRenderer = React.memo((props: { description: string, handlerId: string }) => {
if(!props.description) {
return (
<CenteredTextRenderer key={"no-description"}>
<Translatable>Channel has no description</Translatable>
</CenteredTextRenderer>
)
}
return (
<div key={"description"} className={cssStyle.descriptionContainer}>
<BBCodeRenderer settings={{ convertSingleUrls: false }} message={props.description} handlerId={props.handlerId} />
</div>
)
});
const DescriptionErrorRenderer = React.memo((props: { error: string }) => (
<CenteredTextRenderer className={cssStyle.error}>
<Translatable>An error happened while fetching the channel description:</Translatable><br />
{props.error}
</CenteredTextRenderer>
));
const PermissionErrorRenderer = React.memo((props: { failedPermission: string }) => (
<CenteredTextRenderer>
<Translatable>You don't have the permission to watch the channel description.</Translatable>&nbsp;
<VariadicTranslatable text={"(Missing permission {})"}><code>{props.failedPermission}</code></VariadicTranslatable>
</CenteredTextRenderer>
));
export const ChannelDescriptionRenderer = React.memo((props: { events: Registry<ChannelDescriptionUiEvents> }) => {
const [ description, setDescription ] = useState<ChannelDescriptionStatus | { status: "loading" }>(() => {
props.events.fire("query_description");
return { status: "loading" };
});
props.events.reactUse("notify_description", event => setDescription(event.status));
switch (description.status) {
case "success":
return (
<DescriptionRenderer description={description.description} key={"description"} handlerId={description.handlerId} />
);
case "error":
return (
<DescriptionErrorRenderer error={description.reason} key={"error"} />
);
case "no-permissions":
return (
<PermissionErrorRenderer failedPermission={description.failedPermission} />
);
case "loading":
default:
return (
<CenteredTextRenderer key={"loading"}>
<Translatable>loading channel description</Translatable> <LoadingDots />
</CenteredTextRenderer>
);
}
});

View File

@ -51,7 +51,7 @@ export class SideHeaderController {
}); });
this.uiEvents.on("action_switch_channel_chat", () => { this.uiEvents.on("action_switch_channel_chat", () => {
this.connection.getSideBar().showChannelConversations(); this.connection.getSideBar().showChannel();
}); });
this.uiEvents.on("action_bot_manage", () => { this.uiEvents.on("action_bot_manage", () => {

View File

@ -3,15 +3,15 @@ import {useRef, useState} from "react";
import {Registry} from "tc-shared/events"; import {Registry} from "tc-shared/events";
import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {Translatable} from "tc-shared/ui/react-elements/i18n";
import {FlatInputField} from "tc-shared/ui/react-elements/InputField"; import {FlatInputField} from "tc-shared/ui/react-elements/InputField";
import {EventType} from "tc-shared/ui/frames/log/Definitions";
import {
getRegisteredNotificationDispatchers,
isNotificationEnabled
} from "tc-shared/ui/frames/log/DispatcherNotifications";
import {Settings, settings} from "tc-shared/settings"; import {Settings, settings} from "tc-shared/settings";
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox"; import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
import {Tooltip} from "tc-shared/ui/react-elements/Tooltip"; import {Tooltip} from "tc-shared/ui/react-elements/Tooltip";
import {isFocusRequestEnabled} from "tc-shared/ui/frames/log/DispatcherFocus"; import {TypeInfo} from "tc-shared/connectionlog/Definitions";
import {
getRegisteredNotificationDispatchers,
isNotificationEnabled
} from "tc-shared/connectionlog/DispatcherNotifications";
import {isFocusRequestEnabled} from "tc-shared/connectionlog/DispatcherFocus";
const cssStyle = require("./Notifications.scss"); const cssStyle = require("./Notifications.scss");
@ -21,7 +21,7 @@ interface EventGroup {
key: string; key: string;
name: string; name: string;
events?: string[]; events?: (keyof TypeInfo)[];
subgroups?: EventGroup[]; subgroups?: EventGroup[];
} }
@ -340,23 +340,23 @@ const knownEventGroups: EventGroup[] = [
key: "client-messages", key: "client-messages",
name: "Messages", name: "Messages",
events: [ events: [
EventType.CLIENT_POKE_RECEIVED, "client.poke.received",
EventType.CLIENT_POKE_SEND, "client.poke.send",
EventType.PRIVATE_MESSAGE_SEND, "private.message.send",
EventType.PRIVATE_MESSAGE_RECEIVED "private.message.received"
] ]
}, },
{ {
key: "client-view", key: "client-view",
name: "View", name: "View",
events: [ events: [
EventType.CLIENT_VIEW_ENTER, "client.view.enter",
EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL, "client.view.enter.own.channel",
EventType.CLIENT_VIEW_MOVE, "client.view.move",
EventType.CLIENT_VIEW_MOVE_OWN, "client.view.move.own",
EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL, "client.view.move.own.channel",
EventType.CLIENT_VIEW_LEAVE, "client.view.leave",
EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL "client.view.leave.own.channel"
] ]
} }
] ]
@ -365,45 +365,45 @@ const knownEventGroups: EventGroup[] = [
key: "server", key: "server",
name: "Server", name: "Server",
events: [ events: [
EventType.GLOBAL_MESSAGE, "global.message",
EventType.SERVER_CLOSED, "server.closed",
EventType.SERVER_BANNED, "server.banned",
] ]
}, },
{ {
key: "connection", key: "connection",
name: "Connection", name: "Connection",
events: [ events: [
EventType.CONNECTION_BEGIN, "connection.begin",
EventType.CONNECTION_CONNECTED, "connection.connected",
EventType.CONNECTION_FAILED "connection.failed"
] ]
} }
]; ];
const groupNames: { [key: string]: string } = {}; const groupNames: { [T in keyof TypeInfo]?: string } = {};
groupNames[EventType.CLIENT_POKE_RECEIVED] = tr("You received a poke"); groupNames["client.poke.received"] = tr("You received a poke");
groupNames[EventType.CLIENT_POKE_SEND] = tr("You send a poke"); groupNames["client.poke.send"] = tr("You send a poke");
groupNames[EventType.PRIVATE_MESSAGE_SEND] = tr("You received a private message"); groupNames["private.message.send"] = tr("You received a private message");
groupNames[EventType.PRIVATE_MESSAGE_RECEIVED] = tr("You send a private message"); groupNames["private.message.received"] = tr("You send a private message");
groupNames[EventType.CLIENT_VIEW_ENTER] = tr("A client enters your view"); groupNames["client.view.enter"] = tr("A client enters your view");
groupNames[EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL] = tr("A client enters your view and your channel"); groupNames["client.view.enter.own.channel"] = tr("A client enters your view and your channel");
groupNames[EventType.CLIENT_VIEW_MOVE] = tr("A client switches/gets moved/kicked"); groupNames["client.view.move"] = tr("A client switches/gets moved/kicked");
groupNames[EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL] = tr("A client switches/gets moved/kicked in to/out of your channel"); groupNames["client.view.move.own.channel"] = tr("A client switches/gets moved/kicked in to/out of your channel");
groupNames[EventType.CLIENT_VIEW_MOVE_OWN] = tr("You've been moved or kicked"); groupNames["client.view.move.own"] = tr("You've been moved or kicked");
groupNames[EventType.CLIENT_VIEW_LEAVE] = tr("A client leaves/disconnects of your view"); groupNames["client.view.leave"] = tr("A client leaves/disconnects of your view");
groupNames[EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL] = tr("A client leaves/disconnects of your channel"); groupNames["client.view.leave.own.channel"] = tr("A client leaves/disconnects of your channel");
groupNames[EventType.GLOBAL_MESSAGE] = tr("A server message has been send"); groupNames["global.message"] = tr("A server message has been send");
groupNames[EventType.SERVER_CLOSED] = tr("The server has been closed"); groupNames["server.closed"] = tr("The server has been closed");
groupNames[EventType.SERVER_BANNED] = tr("You've been banned from the server"); groupNames["server.banned"] = tr("You've been banned from the server");
groupNames[EventType.CONNECTION_BEGIN] = tr("You're connecting to a server"); groupNames["connection.begin"] = tr("You're connecting to a server");
groupNames[EventType.CONNECTION_CONNECTED] = tr("You've successfully connected to the server"); groupNames["connection.connected"] = tr("You've successfully connected to the server");
groupNames[EventType.CONNECTION_FAILED] = tr("You're connect attempt failed"); groupNames["connection.failed"] = tr("You're connect attempt failed");
function initializeController(events: Registry<NotificationSettingsEvents>) { function initializeController(events: Registry<NotificationSettingsEvents>) {
let filter = undefined; let filter = undefined;

View File

@ -258,6 +258,8 @@ class ChannelTreeController {
this.sendChannelInfo(event.newChannel); this.sendChannelInfo(event.newChannel);
this.sendChannelStatusIcon(event.newChannel); this.sendChannelStatusIcon(event.newChannel);
this.sendChannelTreeEntries(); this.sendChannelTreeEntries();
this.sendClientTalkStatus(event.client);
} }
@EventHandler<ChannelTreeEvents>("notify_selected_entry_changed") @EventHandler<ChannelTreeEvents>("notify_selected_entry_changed")

View File

@ -14,7 +14,6 @@ import * as log from "tc-shared/log";
import {LogCategory, logDebug, logError, logTrace} from "tc-shared/log"; import {LogCategory, logDebug, logError, logTrace} from "tc-shared/log";
import {Regex} from "tc-shared/ui/modal/ModalConnect"; import {Regex} from "tc-shared/ui/modal/ModalConnect";
import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler"; import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
import {EventType} from "tc-shared/ui/frames/log/Definitions";
import {WrappedWebSocket} from "tc-backend/web/connection/WrappedWebSocket"; import {WrappedWebSocket} from "tc-backend/web/connection/WrappedWebSocket";
import {AbstractVoiceConnection} from "tc-shared/connection/VoiceConnection"; import {AbstractVoiceConnection} from "tc-shared/connection/VoiceConnection";
import {parseCommand} from "tc-backend/web/connection/CommandParser"; import {parseCommand} from "tc-backend/web/connection/CommandParser";
@ -27,7 +26,6 @@ import {ServerFeature} from "tc-shared/connection/ServerFeatures";
import {RTCConnection} from "tc-shared/connection/rtc/Connection"; import {RTCConnection} from "tc-shared/connection/rtc/Connection";
import {RtpVideoConnection} from "tc-shared/connection/rtc/video/Connection"; import {RtpVideoConnection} from "tc-shared/connection/rtc/video/Connection";
import { tr } from "tc-shared/i18n/localize"; import { tr } from "tc-shared/i18n/localize";
import {createErrorModal} from "tc-shared/ui/elements/Modal";
class ReturnListener<T> { class ReturnListener<T> {
resolve: (value?: T | PromiseLike<T>) => void; resolve: (value?: T | PromiseLike<T>) => void;
@ -286,7 +284,7 @@ export class ServerConnection extends AbstractServerConnection {
private startHandshake() { private startHandshake() {
this.updateConnectionState(ConnectionState.INITIALISING); this.updateConnectionState(ConnectionState.INITIALISING);
this.client.log.log(EventType.CONNECTION_LOGIN, {}); this.client.log.log("connection.login", {});
this.handshakeHandler.initialize(); this.handshakeHandler.initialize();
this.handshakeHandler.startHandshake(); this.handshakeHandler.startHandshake();
} }

View File

@ -15,7 +15,6 @@ import {ConnectionStatistics, ServerConnectionEvents} from "tc-shared/connection
import {ConnectionState} from "tc-shared/ConnectionHandler"; import {ConnectionState} from "tc-shared/ConnectionHandler";
import {VoiceBridge, VoicePacket, VoiceWhisperPacket} from "./bridge/VoiceBridge"; import {VoiceBridge, VoicePacket, VoiceWhisperPacket} from "./bridge/VoiceBridge";
import {NativeWebRTCVoiceBridge} from "./bridge/NativeWebRTCVoiceBridge"; import {NativeWebRTCVoiceBridge} from "./bridge/NativeWebRTCVoiceBridge";
import {EventType} from "tc-shared/ui/frames/log/Definitions";
import { import {
kUnknownWhisperClientUniqueId, kUnknownWhisperClientUniqueId,
WhisperSession, WhisperSession,
@ -191,7 +190,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
}, payload))) }, payload)))
}; };
this.voiceBridge.callbackDisconnect = () => { this.voiceBridge.callbackDisconnect = () => {
this.connection.client.log.log(EventType.CONNECTION_VOICE_DROPPED, { }); this.connection.client.log.log("connection.voice.dropped", { });
if(!this.connectionLostModalOpen) { if(!this.connectionLostModalOpen) {
this.connectionLostModalOpen = true; this.connectionLostModalOpen = true;
const modal = createErrorModal(tr("Voice connection lost"), tr("Lost voice connection to the target server. Trying to reconnect...")); const modal = createErrorModal(tr("Voice connection lost"), tr("Lost voice connection to the target server. Trying to reconnect..."));
@ -202,14 +201,14 @@ export class VoiceConnection extends AbstractVoiceConnection {
this.executeVoiceBridgeReconnect(); this.executeVoiceBridgeReconnect();
} }
this.connection.client.log.log(EventType.CONNECTION_VOICE_CONNECT, { attemptCount: this.connectAttemptCounter }); this.connection.client.log.log("connection.voice.connect", { attemptCount: this.connectAttemptCounter });
this.setConnectionState(VoiceConnectionStatus.Connecting); this.setConnectionState(VoiceConnectionStatus.Connecting);
this.voiceBridge.connect().then(result => { this.voiceBridge.connect().then(result => {
if(result.type === "success") { if(result.type === "success") {
this.lastConnectAttempt = 0; this.lastConnectAttempt = 0;
this.connectAttemptCounter = 0; this.connectAttemptCounter = 0;
this.connection.client.log.log(EventType.CONNECTION_VOICE_CONNECT_SUCCEEDED, { }); this.connection.client.log.log("connection.voice.connect.succeeded", { });
const currentInput = this.voiceRecorder()?.input; const currentInput = this.voiceRecorder()?.input;
if(currentInput) { if(currentInput) {
this.voiceBridge.setInput(currentInput).catch(error => { this.voiceBridge.setInput(currentInput).catch(error => {
@ -226,7 +225,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
let doReconnect = result.allowReconnect && this.connectAttemptCounter < 5; let doReconnect = result.allowReconnect && this.connectAttemptCounter < 5;
logWarn(LogCategory.VOICE, tr("Failed to setup voice bridge: %s. Reconnect: %o"), result.message, doReconnect); logWarn(LogCategory.VOICE, tr("Failed to setup voice bridge: %s. Reconnect: %o"), result.message, doReconnect);
this.connection.client.log.log(EventType.CONNECTION_VOICE_CONNECT_FAILED, { this.connection.client.log.log("connection.voice.connect.failed", {
reason: result.message, reason: result.message,
reconnect_delay: doReconnect ? 1 : 0 reconnect_delay: doReconnect ? 1 : 0
}); });