From 3412faf125ff4355a9c4f203f747bdcc3af1aa2b Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Fri, 18 Dec 2020 17:06:38 +0100 Subject: [PATCH] A lot of updates and added a new feature --- ChangeLog.md | 7 + shared/css/static/main-layout.scss | 2 + shared/js/ConnectionHandler.ts | 38 +- shared/js/ConnectionManager.ts | 18 +- shared/js/SideBarManager.ts | 6 +- shared/js/connection/CommandHandler.ts | 44 ++- shared/js/connection/ErrorCode.ts | 1 + shared/js/connectionlog/Definitions.ts | 345 +++++++++++++++++ .../log => connectionlog}/DispatcherFocus.ts | 8 +- .../DispatcherNotifications.ts | 70 ++-- shared/js/connectionlog/ServerEventLog.tsx | 61 +++ shared/js/conversations/AbstractConversion.ts | 30 +- .../ChannelConversationManager.ts | 14 +- shared/js/text/bbcode.tsx | 23 +- shared/js/text/bbcode/renderer.ts | 4 +- shared/js/text/bbcode/url.tsx | 35 +- shared/js/text/chat.ts | 1 - shared/js/tree/Channel.ts | 130 ++++--- shared/js/tree/ChannelTree.tsx | 28 +- shared/js/tree/Client.ts | 10 +- shared/js/tree/Server.ts | 2 +- shared/js/ui/frames/SideBarController.ts | 20 +- shared/js/ui/frames/SideBarDefinitions.ts | 9 +- shared/js/ui/frames/SideBarRenderer.tsx | 27 +- shared/js/ui/frames/log/Controller.ts | 49 +++ shared/js/ui/frames/log/Definitions.ts | 354 +----------------- shared/js/ui/frames/log/Renderer.scss | 2 +- shared/js/ui/frames/log/Renderer.tsx | 59 ++- .../{DispatcherLog.tsx => RendererEvent.tsx} | 116 +++--- shared/js/ui/frames/log/ServerEventLog.tsx | 86 ----- .../side/AbstractConversationController.ts | 24 +- .../side/AbstractConversationRenderer.scss | 3 + .../side/AbstractConversationRenderer.tsx | 6 +- .../js/ui/frames/side/ChannelBarController.ts | 194 ++++++++++ .../ui/frames/side/ChannelBarDefinitions.ts | 36 ++ .../js/ui/frames/side/ChannelBarRenderer.tsx | 89 +++++ .../side/ChannelConversationController.ts | 13 - .../side/ChannelDescriptionController.ts | 121 ++++++ .../side/ChannelDescriptionDefinitions.ts | 18 + .../side/ChannelDescriptionRenderer.scss | 51 +++ .../side/ChannelDescriptionRenderer.tsx | 86 +++++ shared/js/ui/frames/side/HeaderController.ts | 2 +- shared/js/ui/modal/settings/Notifications.tsx | 84 ++--- shared/js/ui/tree/Controller.tsx | 2 + web/app/connection/ServerConnection.ts | 4 +- web/app/legacy/voice/VoiceHandler.ts | 9 +- 46 files changed, 1568 insertions(+), 773 deletions(-) create mode 100644 shared/js/connectionlog/Definitions.ts rename shared/js/{ui/frames/log => connectionlog}/DispatcherFocus.ts (63%) rename shared/js/{ui/frames/log => connectionlog}/DispatcherNotifications.ts (89%) create mode 100644 shared/js/connectionlog/ServerEventLog.tsx create mode 100644 shared/js/ui/frames/log/Controller.ts rename shared/js/ui/frames/log/{DispatcherLog.tsx => RendererEvent.tsx} (86%) delete mode 100644 shared/js/ui/frames/log/ServerEventLog.tsx create mode 100644 shared/js/ui/frames/side/ChannelBarController.ts create mode 100644 shared/js/ui/frames/side/ChannelBarDefinitions.ts create mode 100644 shared/js/ui/frames/side/ChannelBarRenderer.tsx create mode 100644 shared/js/ui/frames/side/ChannelDescriptionController.ts create mode 100644 shared/js/ui/frames/side/ChannelDescriptionDefinitions.ts create mode 100644 shared/js/ui/frames/side/ChannelDescriptionRenderer.scss create mode 100644 shared/js/ui/frames/side/ChannelDescriptionRenderer.tsx diff --git a/ChangeLog.md b/ChangeLog.md index dce6bd4c..1c114acc 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,11 @@ # 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** - Directly connection when hitting enter on the address line diff --git a/shared/css/static/main-layout.scss b/shared/css/static/main-layout.scss index 6d6dc432..d65b1a08 100644 --- a/shared/css/static/main-layout.scss +++ b/shared/css/static/main-layout.scss @@ -134,6 +134,8 @@ html:root { min-height: 0; width: 100%; + overflow: hidden; + border-radius: 5px 5px 0 0; padding-right: 5px; diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 9c1ee6f7..b22297bf 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -24,8 +24,6 @@ import {FileTransferState, TransferProvider} from "./file/Transfer"; import {traj, tr} from "./i18n/localize"; import {md5} from "./crypto/md5"; 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 {W2GPluginCmdHandler} from "./video-viewer/W2GPlugin"; import {VoiceConnectionStatus, WhisperSessionInitializeData} from "./connection/VoiceConnection"; @@ -41,6 +39,8 @@ import {ChannelConversationManager} from "./conversations/ChannelConversationMan import {PrivateConversationManager} from "tc-shared/conversations/PrivateConversationManager"; import {SelectedClientInfo} from "./SelectedClientInfo"; import {SideBarManager} from "tc-shared/SideBarManager"; +import {ServerEventLog} from "tc-shared/connectionlog/ServerEventLog"; +import {EventType} from "tc-shared/connectionlog/Definitions"; export enum InputHardwareState { MISSING, @@ -270,7 +270,7 @@ export class ConnectionHandler { } } 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: { server_hostname: server_address.host, server_port: server_address.port @@ -303,7 +303,7 @@ export class ConnectionHandler { 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)) { const id = ++this._connect_initialize_id; - this.log.log(EventType.CONNECTION_HOSTNAME_RESOLVE, {}); + this.log.log("connection.hostname.resolve", {}); try { const resolved = await dns.resolve_address(server_address, { timeout: 5000 }) || {} as any; 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.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: { server_port: server_address.port, server_hostname: server_address.host @@ -348,7 +348,7 @@ export class ConnectionHandler { log.warn(LogCategory.CLIENT, tr("Failed to successfully disconnect from server: {}"), error); } this.sound.play(Sound.CONNECTION_DISCONNECTED); - this.log.log(EventType.DISCONNECTED, {}); + this.log.log("disconnected", {}); } getClient() : LocalClientEntry { return this.localClient; } @@ -386,7 +386,7 @@ export class ConnectionHandler { this.connection_state = event.newState; if(event.newState === ConnectionState.CONNECTED) { log.info(LogCategory.CLIENT, tr("Client connected")); - this.log.log(EventType.CONNECTION_CONNECTED, { + this.log.log("connection.connected", { serverAddress: { server_port: this.channelTree.server.remote_address.port, server_hostname: this.channelTree.server.remote_address.host @@ -487,12 +487,12 @@ export class ConnectionHandler { case DisconnectReason.HANDLER_DESTROYED: if(data) { this.sound.play(Sound.CONNECTION_DISCONNECTED); - this.log.log(EventType.DISCONNECTED, {}); + this.log.log("disconnected", {}); } break; case DisconnectReason.DNS_FAILED: 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 }); this.sound.play(Sound.CONNECTION_REFUSED); @@ -539,7 +539,7 @@ export class ConnectionHandler { this._certificate_modal.open(); }); } - this.log.log(EventType.CONNECTION_FAILED, { + this.log.log("connection.failed", { serverAddress: { server_hostname: this.serverConnection.remote_address().host, server_port: this.serverConnection.remote_address().port @@ -594,7 +594,7 @@ export class ConnectionHandler { break; case DisconnectReason.SERVER_CLOSED: - this.log.log(EventType.SERVER_CLOSED, {message: data.reasonmsg}); + this.log.log("server.closed", {message: data.reasonmsg}); createErrorModal( tr("Server closed"), @@ -606,7 +606,7 @@ export class ConnectionHandler { auto_reconnect = true; break; 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 => { if(!(typeof password === "string")) return; @@ -647,7 +647,7 @@ export class ConnectionHandler { this.sound.play(Sound.CONNECTION_BANNED); break; case DisconnectReason.CLIENT_BANNED: - this.log.log(EventType.SERVER_BANNED, { + this.log.log("server.banned", { invoker: { client_name: data["invokername"], 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...")); 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")); const server_address = this.serverConnection.remote_address(); @@ -686,7 +686,7 @@ export class ConnectionHandler { this._reconnect_timer = setTimeout(() => { this._reconnect_timer = undefined; - this.log.log(EventType.RECONNECT_EXECUTE, {}); + this.log.log("reconnect.execute", {}); 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})); @@ -698,7 +698,7 @@ export class ConnectionHandler { cancel_reconnect(log_event: boolean) { 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); this._reconnect_timer = undefined; } @@ -791,7 +791,7 @@ export class ConnectionHandler { this.clientStatusSync = true; this.serverConnection.send_command("clientupdate", localClientUpdates).catch(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; }); } @@ -837,7 +837,7 @@ export class ConnectionHandler { //client_output_hardware: this.client_status.sound_playback_supported }).catch(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 : "", }).catch(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", { diff --git a/shared/js/ConnectionManager.ts b/shared/js/ConnectionManager.ts index 71df28cf..524fc7ec 100644 --- a/shared/js/ConnectionManager.ts +++ b/shared/js/ConnectionManager.ts @@ -6,6 +6,8 @@ import {FooterRenderer} from "tc-shared/ui/frames/footer/Renderer"; import * as React from "react"; import * as ReactDOM from "react-dom"; 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; @@ -30,33 +32,36 @@ export class ConnectionManager { private connection_handlers: ConnectionHandler[] = []; private active_handler: ConnectionHandler | undefined; - private _container_log_server: JQuery; private _container_channel_tree: JQuery; private _container_hostbanner: JQuery; private containerChannelVideo: ReplaceableContainer; private containerSideBar: HTMLDivElement; private containerFooter: HTMLDivElement; + private containerServerLog: HTMLDivElement; private sideBarController: SideBarController; + private serverLogController: ServerEventLogController; constructor() { this.event_registry = new Registry(); this.event_registry.enableDebug("connection-manager"); this.sideBarController = new SideBarController(); + this.serverLogController = new ServerEventLogController(); 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_hostbanner = $("#hostbanner"); - this.containerFooter = document.getElementById("container-footer") as HTMLDivElement; this.sideBarController.renderInto(document.getElementById("chat") as HTMLDivElement); this.set_active_connection(undefined); } - initializeFooter() { + initializeReactComponents() { ReactDOM.render(React.createElement(FooterRenderer), this.containerFooter); + ReactDOM.render(React.createElement(ServerLogFrame, { events: this.serverLogController.events }), this.containerServerLog); } events() : Registry { @@ -117,16 +122,15 @@ export class ConnectionManager { private set_active_connection_(handler: ConnectionHandler) { this.sideBarController.setConnection(handler); + this.serverLogController.setConnectionHandler(handler); this._container_channel_tree.children().detach(); - this._container_log_server.children().detach(); this._container_hostbanner.children().detach(); this.containerChannelVideo.replaceWith(handler?.video_frame.getContainer()); if(handler) { this._container_hostbanner.append(handler.hostbanner.html_tag); this._container_channel_tree.append(handler.channelTree.tag_tree()); - this._container_log_server.append(handler.log.getHTMLTag()); } const old_handler = this.active_handler; @@ -184,7 +188,7 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { name: "server manager init", function: async () => { server_connections = new ConnectionManager(); - server_connections.initializeFooter(); + server_connections.initializeReactComponents(); }, priority: 80 }); diff --git a/shared/js/SideBarManager.ts b/shared/js/SideBarManager.ts index d79073d2..f0487177 100644 --- a/shared/js/SideBarManager.ts +++ b/shared/js/SideBarManager.ts @@ -16,7 +16,7 @@ export class SideBarManager { constructor(connection: ConnectionHandler) { this.events = new Registry(); this.connection = connection; - this.currentType = "channel-chat"; + this.currentType = "channel"; } destroy() {} @@ -38,8 +38,8 @@ export class SideBarManager { this.setSideBarContent("private-chat"); } - showChannelConversations() { - this.setSideBarContent("channel-chat"); + showChannel() { + this.setSideBarContent("channel"); } showClientInfo(client: ClientEntry) { diff --git a/shared/js/connection/CommandHandler.ts b/shared/js/connection/CommandHandler.ts index 85613bdd..1208fae2 100644 --- a/shared/js/connection/CommandHandler.ts +++ b/shared/js/connection/CommandHandler.ts @@ -1,5 +1,5 @@ import * as log from "../log"; -import {LogCategory, logError} from "../log"; +import {LogCategory, logError, logWarn} from "../log"; import {AbstractServerConnection, CommandOptions, ServerCommand} from "../connection/ConnectionBase"; import {Sound} from "../sound/Sounds"; 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 {renderBBCodeAsJQuery} from "../text/bbcode"; import {tr} from "../i18n/localize"; -import {EventClient, EventType} from "../ui/frames/log/Definitions"; import {ErrorCode} from "../connection/ErrorCode"; import {server_connections} from "tc-shared/ConnectionManager"; import {ChannelEntry} from "tc-shared/tree/Channel"; +import {EventClient} from "tc-shared/connectionlog/Definitions"; export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss { constructor(connection: AbstractServerConnection) { @@ -56,6 +56,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { this["initserver"] = this.handleCommandServerInit; this["notifychannelmoved"] = this.handleNotifyChannelMoved; this["notifychanneledited"] = this.handleNotifyChannelEdited; + this["notifychanneldescriptionchanged"] = this.handleNotifyChannelDescriptionChanged; this["notifytextmessage"] = this.handleNotifyTextMessage; this["notifyclientchatcomposing"] = this.notifyClientChatComposing; this["notifyclientchatclosed"] = this.handleNotifyClientChatClosed; @@ -116,18 +117,18 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { if(res.id == ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) { //Permission error 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"); - 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) }); this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS); } 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 }); } } } 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 { 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) { /* show in log */ 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 }); } else { @@ -219,7 +220,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { if(properties.virtualserver_hostmessage_mode == 3) { /* first let the client initialize his stuff */ setTimeout(() => { - this.connection_handler.log.log(EventType.SERVER_HOST_MESSAGE_DISCONNECT, { + this.connection_handler.log.log("server.host.message.disconnect", { message: properties.virtualserver_welcomemessage }); @@ -233,7 +234,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { /* welcome message */ 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 }); } @@ -504,7 +505,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { if(this.connection_handler.areQueriesShown() || client.properties.client_type != ClientType.CLIENT_QUERY) { 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_to: channel ? channel.log_data() : undefined, client: client.log_data(), @@ -592,7 +593,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { let channel_to = tree.findChannel(targetChannelId); 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_to: channel_to ? channel_to.log_data() : undefined, client: client.log_data(), @@ -673,7 +674,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { } 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, { channel_from: channelFrom ? { 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) { json = json[0]; //Only one bulk @@ -815,7 +829,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { }); if(targetIsOwn) { 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"], sender: { client_unique_id: json["invokeruid"], @@ -825,7 +839,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { }); } else { 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"], target: { 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 conversations = this.connection_handler.getChannelConversations(); - this.connection_handler.log.log(EventType.GLOBAL_MESSAGE, { + this.connection_handler.log.log("global.message", { isOwnMessage: invoker instanceof LocalClientEntry, message: json["msg"], sender: { @@ -976,7 +990,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { unique_id: json["invokeruid"] }, 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"]), message: json["msg"] }); diff --git a/shared/js/connection/ErrorCode.ts b/shared/js/connection/ErrorCode.ts index 1b49c186..5ab02bcd 100644 --- a/shared/js/connection/ErrorCode.ts +++ b/shared/js/connection/ErrorCode.ts @@ -172,6 +172,7 @@ export enum ErrorCode { CUSTOM_ERROR = 0xFFFF, + /** @deprecated Use SERVER_INSUFFICIENT_PERMISSIONS */ PERMISSION_ERROR = ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS, EMPTY_RESULT = ErrorCode.DATABASE_EMPTY_RESULT } \ No newline at end of file diff --git a/shared/js/connectionlog/Definitions.ts b/shared/js/connectionlog/Definitions.ts new file mode 100644 index 00000000..e12cd726 --- /dev/null +++ b/shared/js/connectionlog/Definitions.ts @@ -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 { + 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": {} +} \ No newline at end of file diff --git a/shared/js/ui/frames/log/DispatcherFocus.ts b/shared/js/connectionlog/DispatcherFocus.ts similarity index 63% rename from shared/js/ui/frames/log/DispatcherFocus.ts rename to shared/js/connectionlog/DispatcherFocus.ts index 35e90e06..afd72258 100644 --- a/shared/js/ui/frames/log/DispatcherFocus.ts +++ b/shared/js/connectionlog/DispatcherFocus.ts @@ -1,8 +1,8 @@ -import {EventType} from "../../../ui/frames/log/Definitions"; -import {Settings, settings} from "../../../settings"; +import {settings, Settings} from "tc-shared/settings"; +import {EventType, TypeInfo} from "tc-shared/connectionlog/Definitions"; -const focusDefaultStatus = {}; -focusDefaultStatus[EventType.CLIENT_POKE_RECEIVED] = true; +const focusDefaultStatus: {[T in keyof TypeInfo]?: boolean} = {}; +focusDefaultStatus["client.poke.received"] = true; export function requestWindowFocus() { if(__build.target === "web") { diff --git a/shared/js/ui/frames/log/DispatcherNotifications.ts b/shared/js/connectionlog/DispatcherNotifications.ts similarity index 89% rename from shared/js/ui/frames/log/DispatcherNotifications.ts rename to shared/js/connectionlog/DispatcherNotifications.ts index 6ad0bd20..b7ef40f8 100644 --- a/shared/js/ui/frames/log/DispatcherNotifications.ts +++ b/shared/js/connectionlog/DispatcherNotifications.ts @@ -1,29 +1,27 @@ import * as loader 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 {getIconManager} from "tc-shared/file/Icons"; 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 = (data: TypeInfo[T], handlerId: string, eventType: T) => void; -const notificationDefaultStatus = {}; -notificationDefaultStatus[EventType.CLIENT_POKE_RECEIVED] = true; -notificationDefaultStatus[EventType.SERVER_BANNED] = true; -notificationDefaultStatus[EventType.SERVER_CLOSED] = true; -notificationDefaultStatus[EventType.SERVER_HOST_MESSAGE_DISCONNECT] = true; -notificationDefaultStatus[EventType.GLOBAL_MESSAGE] = true; -notificationDefaultStatus[EventType.CONNECTION_FAILED] = true; -notificationDefaultStatus[EventType.PRIVATE_MESSAGE_RECEIVED] = true; -notificationDefaultStatus[EventType.CONNECTION_VOICE_DROPPED] = true; +const notificationDefaultStatus: {[T in keyof TypeInfo]?: boolean} = {}; +notificationDefaultStatus["client.poke.received"] = true; +notificationDefaultStatus["server.banned"] = true; +notificationDefaultStatus["server.closed"] = true; +notificationDefaultStatus["server.host.message.disconnect"] = true; +notificationDefaultStatus["global.message"] = true; +notificationDefaultStatus["connection.failed"] = true; +notificationDefaultStatus["private.message.received"] = true; +notificationDefaultStatus["connection.voice.dropped"] = true; let windowFocused = false; @@ -210,7 +208,7 @@ registerDispatcher(EventType.SERVER_BANNED, (data, 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) : - 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) => { let message; @@ -406,7 +404,7 @@ registerDispatcher(EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL, (data, handlerId) => break; 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, { @@ -482,19 +480,19 @@ registerDispatcher(EventType.PRIVATE_MESSAGE_RECEIVED, (data, handlerId) => { }); registerDispatcher(EventType.WEBRTC_FATAL_ERROR, (data, handlerId) => { - if(data.retryTimeout) { - let time = Math.ceil(data.retryTimeout / 1000); - let minutes = Math.floor(time / 60); - let seconds = time % 60; + if(data.retryTimeout) { + let time = Math.ceil(data.retryTimeout / 1000); + let minutes = Math.floor(time / 60); + let seconds = time % 60; - spawnServerNotification(handlerId, { - body: tra("WebRTC connection closed due to a fatal error:\n{}\nRetry scheduled in {}.", data.message, (minutes > 0 ? minutes + "m" : "") + seconds + "s") - }); - } else { - spawnServerNotification(handlerId, { - body: tra("WebRTC connection closed due to a fatal error:\n{}\nNo retry scheduled.", data.message) - }); - } + spawnServerNotification(handlerId, { + body: tra("WebRTC connection closed due to a fatal error:\n{}\nRetry scheduled in {}.", data.message, (minutes > 0 ? minutes + "m" : "") + seconds + "s") + }); + } else { + spawnServerNotification(handlerId, { + body: tra("WebRTC connection closed due to a fatal error:\n{}\nNo retry scheduled.", data.message) + }); + } }); /* snipped PRIVATE_MESSAGE_SEND */ @@ -509,14 +507,14 @@ loader.register_task(Stage.LOADED, { /* yeahr fuck safari */ 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) { 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 => { - 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); }); } }, diff --git a/shared/js/connectionlog/ServerEventLog.tsx b/shared/js/connectionlog/ServerEventLog.tsx new file mode 100644 index 00000000..bd32c2be --- /dev/null +++ b/shared/js/connectionlog/ServerEventLog.tsx @@ -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; + private readonly connection: ConnectionHandler; + + private maxHistoryLength: number = 100; + private eventLog: LogMessage[] = []; + + constructor(connection: ConnectionHandler) { + this.connection = connection; + this.events = new Registry(); + } + + log(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; + } +} \ No newline at end of file diff --git a/shared/js/conversations/AbstractConversion.ts b/shared/js/conversations/AbstractConversion.ts index 7a5a2f2e..45e01a8f 100644 --- a/shared/js/conversations/AbstractConversion.ts +++ b/shared/js/conversations/AbstractConversion.ts @@ -59,7 +59,6 @@ export abstract class AbstractChat { protected errorMessage: string; private conversationMode: ChannelConversationMode; - protected crossChannelChatSupported: boolean = true; protected unreadTimestamp: number; protected unreadState: boolean = false; @@ -338,6 +337,9 @@ export interface AbstractChatManagerEvents { }, notify_unread_count_changed: { unreadConversations: number + }, + notify_cross_conversation_support_changed: { + crossConversationSupported: boolean } } @@ -351,18 +353,23 @@ export abstract class AbstractChatManager(); this.listenerConnection = []; this.currentUnreadCount = 0; + this.crossConversationSupport = true; + this.listenerUnreadTimestamp = () => { let count = this.getConversations().filter(conversation => conversation.isUnread()).length; if(count === this.currentUnreadCount) { return; } @@ -387,6 +394,10 @@ export abstract class AbstractChatManager this.setUnreadTimestamp(unreadTimestamp); 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()); }); } @@ -175,7 +176,6 @@ export class ChannelConversation extends AbstractChat break; case "unsupported": - this.crossChannelChatSupported = false; this.setConversationMode(ChannelConversationMode.Private, false); this.setCurrentMode("normal"); break; @@ -348,6 +348,16 @@ export class ChannelConversationManager extends AbstractChatManager { + 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 => { diff --git a/shared/js/text/bbcode.tsx b/shared/js/text/bbcode.tsx index 30672e26..12a2c1bc 100644 --- a/shared/js/text/bbcode.tsx +++ b/shared/js/text/bbcode.tsx @@ -5,6 +5,7 @@ import {parse as parseBBCode} from "vendor/xbbcode/parser"; import {fixupJQueryUrlTags} from "tc-shared/text/bbcode/url"; import {fixupJQueryImageTags} from "tc-shared/text/bbcode/image"; import "./bbcode.scss"; +import {BBCodeHandlerContext} from "vendor/xbbcode/renderer/react"; export const escapeBBCode = (text: string) => text.replace(/(\[)/g, "\\$1"); @@ -80,11 +81,23 @@ function preprocessMessage(message: string, settings: BBCodeRenderOptions) : str return message; } -export const BBCodeRenderer = (props: { message: string, settings: BBCodeRenderOptions }) => ( - - {preprocessMessage(props.message, props.settings)} - -); +export const BBCodeRenderer = (props: { message: string, settings: BBCodeRenderOptions, handlerId?: string }) => { + if(props.handlerId) { + return ( + + + {preprocessMessage(props.message, props.settings)} + + + ); + } else { + return ( + + {preprocessMessage(props.message, props.settings)} + + ); + } +} export function renderBBCodeAsJQuery(message: string, settings: BBCodeRenderOptions) : JQuery[] { diff --git a/shared/js/text/bbcode/renderer.ts b/shared/js/text/bbcode/renderer.ts index cb913483..add7a512 100644 --- a/shared/js/text/bbcode/renderer.ts +++ b/shared/js/text/bbcode/renderer.ts @@ -8,4 +8,6 @@ export const rendererHTML = new HTMLRenderer(rendererReact); import "./emoji"; import "./highlight"; -import "./youtube"; \ No newline at end of file +import "./youtube"; +import "./url"; +import "./image"; \ No newline at end of file diff --git a/shared/js/text/bbcode/url.tsx b/shared/js/text/bbcode/url.tsx index ccf1c170..56aca68d 100644 --- a/shared/js/text/bbcode/url.tsx +++ b/shared/js/text/bbcode/url.tsx @@ -4,8 +4,10 @@ import * as loader from "tc-loader"; import {ElementRenderer} from "vendor/xbbcode/renderer/base"; import {TagElement} from "vendor/xbbcode/elements"; 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 {ClientTag} from "tc-shared/ui/tree/EntryTags"; +import {useContext} from "react"; function spawnUrlContextMenu(pageX: number, pageY: number, target: string) { 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, { name: "XBBCode code tag init", 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; rendererReact.registerCustomRenderer(new class extends ElementRenderer { render(element: TagElement, renderer: ReactRenderer): React.ReactNode { - let target; - if (!element.options) + let target: string; + if (!element.options) { target = rendererText.render(element); - else + } else { target = element.options; + } regexUrl.lastIndex = 0; - if (!regexUrl.test(target)) + if (!regexUrl.test(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 0 ? clientDatabaseId : undefined} + handlerId={handlerId} + />; + } + } - /* TODO: Implement client URLs */ return { event.preventDefault(); spawnUrlContextMenu(event.pageX, event.pageY, target); diff --git a/shared/js/text/chat.ts b/shared/js/text/chat.ts index 62c77365..09fa189c 100644 --- a/shared/js/text/chat.ts +++ b/shared/js/text/chat.ts @@ -4,7 +4,6 @@ import {renderMarkdownAsBBCode} from "../text/markdown"; import {escapeBBCode} from "../text/bbcode"; import {parse as parseBBCode} from "vendor/xbbcode/parser"; import {TagElement} from "vendor/xbbcode/elements"; -import * as React from "react"; import {regexImage} from "tc-shared/text/bbcode/image"; interface UrlKnifeUrl { diff --git a/shared/js/tree/Channel.ts b/shared/js/tree/Channel.ts index 2885d2b2..83483c0f 100644 --- a/shared/js/tree/Channel.ts +++ b/shared/js/tree/Channel.ts @@ -18,10 +18,10 @@ import {formatMessage} from "../ui/frames/chat"; import {Registry} from "../events"; import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry"; import {spawnFileTransferModal} from "../ui/modal/transfer/ModalFileTransfer"; -import {EventChannelData} from "../ui/frames/log/Definitions"; import {ErrorCode} from "../connection/ErrorCode"; import {ClientIcon} from "svg-sprites/client-icons"; import { tr } from "tc-shared/i18n/localize"; +import {EventChannelData} from "tc-shared/connectionlog/Definitions"; export enum ChannelType { PERMANENT, @@ -45,7 +45,16 @@ export enum ChannelSubscribeMode { export enum ChannelConversationMode { Public = 0, 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 { @@ -79,8 +88,10 @@ export class ChannelProperties { //Only after request channel_description: string = ""; - channel_conversation_mode: ChannelConversationMode = 0; + channel_conversation_mode: ChannelConversationMode = ChannelConversationMode.Public; channel_conversation_history_length: number = -1; + + channel_sidebar_mode: ChannelSidebarMode = ChannelSidebarMode.Unknown; } export interface ChannelEvents extends ChannelTreeEntryEvents { @@ -99,7 +110,8 @@ export interface ChannelEvents extends ChannelTreeEntryEvents { }, notify_collapsed_state_changed: { collapsed: boolean - } + }, + notify_description_changed: {} } export class ParsedChannelName { @@ -173,10 +185,9 @@ export class ChannelEntry extends ChannelTreeEntry { private _destroyed = false; private cachedPasswordHash: string; - private _cached_channel_description: string = undefined; - private _cached_channel_description_promise: Promise = undefined; - private _cached_channel_description_promise_resolve: any = undefined; - private _cached_channel_description_promise_reject: any = undefined; + private channelDescriptionCached: boolean; + private channelDescriptionCallback: ((success: boolean) => void)[]; + private channelDescriptionPromise: Promise; private collapsed: boolean; private subscribed: boolean; @@ -212,18 +223,20 @@ export class ChannelEntry extends ChannelTreeEntry { 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.channelDescriptionCached = false; + this.channelDescriptionCallback = []; } destroy() { this._destroyed = true; + this.channelDescriptionCallback.forEach(callback => callback(false)); + this.channelDescriptionCallback = []; + this.client_list.forEach(e => this.unregisterClient(e, true)); 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.parent = undefined; this.channel_next = undefined; @@ -248,18 +261,39 @@ export class ChannelEntry extends ChannelTreeEntry { return this.parsed_channel_name.text; } - getChannelDescription() : Promise { - if(this._cached_channel_description) return new Promise(resolve => resolve(this._cached_channel_description)); - if(this._cached_channel_description_promise) return this._cached_channel_description_promise; + async getChannelDescription() : Promise { + if(this.channelDescriptionPromise) { + return this.channelDescriptionPromise; + } - this.channelTree.client.serverConnection.send_command("channelgetdescription", {cid: this.channelId}).catch(error => { - this._cached_channel_description_promise_reject(error); - }); + const promise = this.doGetChannelDescription(); + this.channelDescriptionPromise = promise; + promise + .then(() => this.channelDescriptionPromise = undefined) + .catch(() => this.channelDescriptionPromise = undefined); + return promise; + } - return this._cached_channel_description_promise = new Promise((resolve, reject) => { - this._cached_channel_description_promise_resolve = resolve; - this._cached_channel_description_promise_reject = reject; - }); + private async doGetChannelDescription() { + if(!this.channelDescriptionCached) { + 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) { @@ -411,7 +445,7 @@ export class ChannelEntry extends ChannelTreeEntry { callback: () => { const conversation = this.channelTree.client.getChannelConversations().findOrCreateConversation(this.getChannelId()); 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) }, { @@ -575,29 +609,27 @@ export class ChannelEntry extends ChannelTreeEntry { let key = variable.key; let value = variable.value; - if(!JSON.map_field_to(this.properties, value, variable.key)) { - /* no update */ - continue; + const hasUpdate = JSON.map_field_to(this.properties, value, variable.key); + + if(key == "channel_description") { + this.channelDescriptionCached = true; + this.channelDescriptionCallback.forEach(callback => callback(true)); + this.channelDescriptionCallback = []; } - if(key == "channel_name") { - this.parsed_channel_name = new ParsedChannelName(value, this.hasParent()); - } else if(key == "channel_order") { - let order = this.channelTree.findChannel(this.properties.channel_order); - this.channelTree.moveChannel(this, order, this.parent, false); - } else if(key === "channel_icon_id") { - this.properties.channel_icon_id = variable.value as any >>> 0; /* unsigned 32 bit number! */ - } else if(key == "channel_description") { - this._cached_channel_description = undefined; - if(this._cached_channel_description_promise_resolve) - this._cached_channel_description_promise_resolve(value); - this._cached_channel_description_promise = undefined; - 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 + "" }); + if(hasUpdate) { + if(key == "channel_name") { + this.parsed_channel_name = new ParsedChannelName(value, this.hasParent()); + } else if(key == "channel_order") { + let order = this.channelTree.findChannel(this.properties.channel_order); + this.channelTree.moveChannel(this, order, this.parent, false); + } else if(key === "channel_icon_id") { + this.properties.channel_icon_id = variable.value as any >>> 0; /* unsigned 32 bit number! */ + } 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) */ @@ -791,4 +823,14 @@ export class ChannelEntry extends ChannelTreeEntry { 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"); + } } \ No newline at end of file diff --git a/shared/js/tree/ChannelTree.tsx b/shared/js/tree/ChannelTree.tsx index 486c3b85..e7fb9859 100644 --- a/shared/js/tree/ChannelTree.tsx +++ b/shared/js/tree/ChannelTree.tsx @@ -115,6 +115,12 @@ export class ChannelTree { this.tagContainer = $.spawn("div").addClass("channel-tree-container"); 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(); } @@ -177,13 +183,13 @@ export class ChannelTree { if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) { const conversation = this.client.getChannelConversations().findOrCreateConversation(this.selectedEntry.channelId); this.client.getChannelConversations().setSelectedConversation(conversation); - this.client.getSideBar().showChannelConversations(); + this.client.getSideBar().showChannel(); } } else if(this.selectedEntry instanceof ServerEntry) { if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) { const conversation = this.client.getChannelConversations().findOrCreateConversation(0); 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(); } }); + client_menu.push({ + type: contextmenu.MenuEntryType.ENTRY, + icon_class: ClientIcon.ChangeNickname, + name: tr("Send private message"), + callback: () => { + createInputModal(tr("Send private message"), tr("Message:
"), 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({ type: contextmenu.MenuEntryType.ENTRY, diff --git a/shared/js/tree/Client.ts b/shared/js/tree/Client.ts index 28f84c51..2e2d834f 100644 --- a/shared/js/tree/Client.ts +++ b/shared/js/tree/Client.ts @@ -22,7 +22,6 @@ import * as hex from "../crypto/hex"; import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry"; import {spawnClientVolumeChange, spawnMusicBotVolumeChange} from "../ui/modal/ModalChangeVolumeNew"; import {spawnPermissionEditorModal} from "../ui/modal/permission/ModalPermissionEditor"; -import {EventClient, EventType} from "../ui/frames/log/Definitions"; import {W2GPluginCmdHandler} from "../video-viewer/W2GPlugin"; import {global_client_actions} from "../events/GlobalEvents"; 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 {VideoClient} from "tc-shared/connection/VideoConnection"; import { tr } from "tc-shared/i18n/localize"; +import {EventClient} from "tc-shared/connectionlog/Definitions"; export enum ClientType { CLIENT_VOICE, @@ -573,7 +573,7 @@ export class ClientEntry extends ChannelTreeEntry { clid: this.clientId(), msg: result }).then(() => { - this.channelTree.client.log.log(EventType.CLIENT_POKE_SEND, { + this.channelTree.client.log.log("client.poke.send", { target: this.log_data(), message: result }); @@ -771,7 +771,7 @@ export class ClientEntry extends ChannelTreeEntry { if(variable.key == "client_nickname") { if(variable.value !== old_value && typeof(old_value) === "string") { 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(), new_name: variable.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 */ return this.handle.serverConnection.send_command("clientupdate", { client_nickname: new_name }).then(() => { 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(), old_name: old_name, new_name: new_name, @@ -1004,7 +1004,7 @@ export class LocalClientEntry extends ClientEntry { return true; }).catch((e: CommandResult) => { 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 }); return false; diff --git a/shared/js/tree/Server.ts b/shared/js/tree/Server.ts index a0db1dea..6b606ad7 100644 --- a/shared/js/tree/Server.ts +++ b/shared/js/tree/Server.ts @@ -193,7 +193,7 @@ export class ServerEntry extends ChannelTreeEntry { name: tr("Join server text channel"), callback: () => { 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) }, { diff --git a/shared/js/ui/frames/SideBarController.ts b/shared/js/ui/frames/SideBarController.ts index 5b1b9072..b8382841 100644 --- a/shared/js/ui/frames/SideBarController.ts +++ b/shared/js/ui/frames/SideBarController.ts @@ -9,6 +9,7 @@ import * as React from "react"; import {SideBarEvents, SideBarType} from "tc-shared/ui/frames/SideBarDefinitions"; import {Registry} from "tc-shared/events"; import {LogCategory, logWarn} from "tc-shared/log"; +import {ChannelBarController} from "tc-shared/ui/frames/side/ChannelBarController"; export class SideBarController { private readonly uiEvents: Registry; @@ -18,8 +19,8 @@ export class SideBarController { private header: SideHeaderController; private clientInfo: ClientInfoController; - private channelConversations: ChannelConversationController; private privateConversations: PrivateConversationController; + private channelBar: ChannelBarController; constructor() { this.listenerConnection = []; @@ -28,8 +29,8 @@ export class SideBarController { this.uiEvents.on("query_content", () => this.sendContent()); this.uiEvents.on("query_content_data", event => this.sendContentData(event.content)); + this.channelBar = new ChannelBarController(); this.privateConversations = new PrivateConversationController(); - this.channelConversations = new ChannelConversationController(); this.clientInfo = new ClientInfoController(); this.header = new SideHeaderController(); } @@ -45,8 +46,8 @@ export class SideBarController { this.currentConnection = connection; this.header.setConnectionHandler(connection); this.clientInfo.setConnectionHandler(connection); - this.channelConversations.setConnectionHandler(connection); this.privateConversations.setConnectionHandler(connection); + this.channelBar.setConnectionHandler(connection); if(connection) { 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 = undefined; + this.channelBar?.destroy(); + this.channelBar = undefined; + this.clientInfo?.destroy(); this.clientInfo = undefined; this.privateConversations?.destroy(); this.privateConversations = undefined; - - this.channelConversations?.destroy(); - this.channelConversations = undefined; } renderInto(container: HTMLDivElement) { @@ -93,17 +94,16 @@ export class SideBarController { }); break; - case "channel-chat": + case "channel": if(!this.currentConnection) { logWarn(LogCategory.GENERAL, tr("Received channel chat content data request without an active connection.")); return; } this.uiEvents.fire_react("notify_content_data", { - content: "channel-chat", + content: "channel", data: { - events: this.channelConversations["uiEvents"], - handlerId: this.currentConnection.handlerId + events: this.channelBar.uiEvents, } }); break; diff --git a/shared/js/ui/frames/SideBarDefinitions.ts b/shared/js/ui/frames/SideBarDefinitions.ts index 5fc58888..d56e43c1 100644 --- a/shared/js/ui/frames/SideBarDefinitions.ts +++ b/shared/js/ui/frames/SideBarDefinitions.ts @@ -1,17 +1,16 @@ import {Registry} from "tc-shared/events"; import {PrivateConversationUIEvents} from "tc-shared/ui/frames/side/PrivateConversationDefinitions"; -import {AbstractConversationUiEvents} from "./side/AbstractConversationDefinitions"; import {ClientInfoEvents} from "tc-shared/ui/frames/side/ClientInfoDefinitions"; 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? */ -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 { "none": {}, - "channel-chat": { - events: Registry, - handlerId: string + "channel": { + events: Registry }, "private-chat": { events: Registry, diff --git a/shared/js/ui/frames/SideBarRenderer.tsx b/shared/js/ui/frames/SideBarRenderer.tsx index 81ad17b3..4a655295 100644 --- a/shared/js/ui/frames/SideBarRenderer.tsx +++ b/shared/js/ui/frames/SideBarRenderer.tsx @@ -1,12 +1,13 @@ import {SideHeaderEvents, SideHeaderState} from "tc-shared/ui/frames/side/HeaderDefinitions"; import {Registry} from "tc-shared/events"; -import React = require("react"); 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 {useContext, useState} from "react"; import {ClientInfoRenderer} from "tc-shared/ui/frames/side/ClientInfoRenderer"; 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"); @@ -23,17 +24,14 @@ function useContentData(type: T) : SideBarTypeData[T] { return contentData; } -const ContentRendererChannelConversation = () => { - const contentData = useContentData("channel-chat"); +const ContentRendererChannel = () => { + const contentData = useContentData("channel"); if(!contentData) { return null; } return ( - ); }; @@ -63,8 +61,8 @@ const ContentRendererClientInfo = () => { const SideBarFrame = (props: { type: SideBarType }) => { switch (props.type) { - case "channel-chat": - return ; + case "channel": + return ; case "private-chat": return ; @@ -88,7 +86,7 @@ const SideBarHeader = (props: { type: SideBarType, eventsHeader: Registry; diff --git a/shared/js/ui/frames/log/Controller.ts b/shared/js/ui/frames/log/Controller.ts new file mode 100644 index 00000000..9fb6a0c6 --- /dev/null +++ b/shared/js/ui/frames/log/Controller.ts @@ -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; + + private currentConnection: ConnectionHandler; + private listenerConnection: (() => void)[]; + + constructor() { + this.events = new Registry(); + + 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 }); + } +} \ No newline at end of file diff --git a/shared/js/ui/frames/log/Definitions.ts b/shared/js/ui/frames/log/Definitions.ts index 9d442d24..6bf96009 100644 --- a/shared/js/ui/frames/log/Definitions.ts +++ b/shared/js/ui/frames/log/Definitions.ts @@ -1,348 +1,16 @@ -import {PermissionInfo} from "../../../permission/PermissionManager"; -import {ViewReasonId} from "../../../ConnectionHandler"; -import * as React from "react"; -import {ServerEventLog} from "../../../ui/frames/log/ServerEventLog"; +import {LogMessage} from "tc-shared/connectionlog/Definitions"; -/* 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", +export interface ServerEventLogUiEvents { + query_handler_id: {}, + query_log: {}, - 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 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 { - 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": { + notify_log_add: { event: LogMessage }, - "notify_show": {} + notify_log: { + events: LogMessage[] + }, + notify_handler_id: { + handlerId: string | undefined + } } \ No newline at end of file diff --git a/shared/js/ui/frames/log/Renderer.scss b/shared/js/ui/frames/log/Renderer.scss index 19415557..91750d02 100644 --- a/shared/js/ui/frames/log/Renderer.scss +++ b/shared/js/ui/frames/log/Renderer.scss @@ -17,7 +17,7 @@ flex-direction: column; justify-content: flex-start; - min-height: 2em; + min-height: 1em; overflow-x: hidden; overflow-y: auto; diff --git a/shared/js/ui/frames/log/Renderer.tsx b/shared/js/ui/frames/log/Renderer.tsx index f4d0923c..2b9308e1 100644 --- a/shared/js/ui/frames/log/Renderer.tsx +++ b/shared/js/ui/frames/log/Renderer.tsx @@ -1,12 +1,17 @@ -import {LogMessage, ServerLogUIEvents} from "tc-shared/ui/frames/log/Definitions"; import {VariadicTranslatable} from "tc-shared/ui/react-elements/i18n"; 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 {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 HandlerIdContext = React.createContext(undefined); +const EventsContext = React.createContext>(undefined); + const LogFallbackDispatcher = (_unused, __unused, eventType) => (
@@ -15,12 +20,14 @@ const LogFallbackDispatcher = (_unused, __unused, eventType) => (
); -const LogEntryRenderer = React.memo((props: { entry: LogMessage, handlerId: string }) => { - const dispatcher = findLogDispatcher(props.entry.type as any) || LogFallbackDispatcher; - const rendered = dispatcher(props.entry.data, props.handlerId, props.entry.type); +const LogEntryRenderer = React.memo((props: { entry: LogMessage }) => { + const handlerId = useContext(HandlerIdContext); + 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; + } const date = new Date(props.entry.timestamp); return ( @@ -35,25 +42,27 @@ const LogEntryRenderer = React.memo((props: { entry: LogMessage, handlerId: stri ); }); -export const ServerLogRenderer = (props: { events: Registry, handlerId: string }) => { - const [ logs, setLogs ] = useState(() => { - props.events.fire_react("query_log"); +const ServerLogRenderer = () => { + const handlerId = useContext(HandlerIdContext); + const events = useContext(EventsContext); + const [ logs, setLogs ] = useDependentState(() => { + events.fire_react("query_log"); return "loading"; - }); + }, [ handlerId ]); const [ revision, setRevision ] = useState(0); const refContainer = useRef(); const scrollOffset = useRef("bottom"); - props.events.reactUse("notify_log", event => { - const logs = event.log.slice(0); + events.reactUse("notify_log", event => { + const logs = event.events.slice(0); logs.splice(0, Math.max(0, logs.length - 100)); logs.sort((a, b) => a.timestamp - b.timestamp); setLogs(logs); }); - props.events.reactUse("notify_log_add", event => { + events.reactUse("notify_log_add", event => { if(logs === "loading") { return; } @@ -72,10 +81,6 @@ export const ServerLogRenderer = (props: { events: Registry, refContainer.current.scrollTop = scrollOffset.current === "bottom" ? refContainer.current.scrollHeight : scrollOffset.current; }; - props.events.reactUse("notify_show", () => { - requestAnimationFrame(fixScroll); - }); - useEffect(() => { const id = requestAnimationFrame(fixScroll); return () => cancelAnimationFrame(id); @@ -91,7 +96,23 @@ export const ServerLogRenderer = (props: { events: Registry, scrollOffset.current = shouldFollow ? "bottom" : top; }}> - {logs === "loading" ? null : logs.map(e => )} + {logs === "loading" ? null : logs.map(e => )} ); }; + +export const ServerLogFrame = (props: { events: Registry }) => { + const [ handlerId, setHandlerId ] = useState(() => { + props.events.fire("query_handler_id"); + return undefined; + }); + props.events.reactUse("notify_handler_id", event => setHandlerId(event.handlerId)); + + return ( + + + + + + ); +} diff --git a/shared/js/ui/frames/log/DispatcherLog.tsx b/shared/js/ui/frames/log/RendererEvent.tsx similarity index 86% rename from shared/js/ui/frames/log/DispatcherLog.tsx rename to shared/js/ui/frames/log/RendererEvent.tsx index 5da3a902..523ebbd9 100644 --- a/shared/js/ui/frames/log/DispatcherLog.tsx +++ b/shared/js/ui/frames/log/RendererEvent.tsx @@ -1,5 +1,4 @@ import {ViewReasonId} from "tc-shared/ConnectionHandler"; -import {EventChannelData, EventClient, EventType, TypeInfo} from "tc-shared/ui/frames/log/Definitions"; import * as React from "react"; import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n"; 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 {XBBCodeRenderer} from "vendor/xbbcode/react"; 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 cssStyleRenderer = require("./Renderer.scss"); -export type DispatcherLog = (data: TypeInfo[T], handlerId: string, eventType: T) => React.ReactNode; +export type RendererEvent = (data: TypeInfo[T], handlerId: string, eventType: T) => React.ReactNode; -const dispatchers: {[key: string]: DispatcherLog} = { }; -function registerDispatcher(key: T, builder: DispatcherLog) { - dispatchers[key] = builder; +const dispatchers: {[T in keyof TypeInfo]?: RendererEvent} = { }; +function registerRenderer(key: T, builder: RendererEvent) { + dispatchers[key] = builder as any; } -export function findLogDispatcher(type: T) : DispatcherLog { - return dispatchers[type]; +export function findLogEventRenderer(type: T) : RendererEvent { + return dispatchers[type] as any; } -export function getRegisteredLogDispatchers() : TypeInfo[] { +export function getRegisteredLogEventRenderer() : TypeInfo[] { return Object.keys(dispatchers) as any; } @@ -46,66 +46,66 @@ const ChannelRenderer = (props: { channel: EventChannelData, handlerId: string, /> ); -registerDispatcher(EventType.ERROR_CUSTOM, data =>
{data.message}
); +registerRenderer(EventType.ERROR_CUSTOM, data =>
{data.message}
); -registerDispatcher(EventType.CONNECTION_BEGIN, data => ( +registerRenderer(EventType.CONNECTION_BEGIN, data => ( <>{data.address.server_hostname} <>{data.address.server_port == 9987 ? "" : (":" + data.address.server_port)} )); -registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVE, () => ( +registerRenderer(EventType.CONNECTION_HOSTNAME_RESOLVE, () => ( Resolving hostname )); -registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVED, data => ( +registerRenderer(EventType.CONNECTION_HOSTNAME_RESOLVED, data => ( <>{data.address.server_hostname} <>{data.address.server_port} )); -registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVE_ERROR, data => ( +registerRenderer(EventType.CONNECTION_HOSTNAME_RESOLVE_ERROR, data => ( <>{data.message} )); -registerDispatcher(EventType.CONNECTION_LOGIN, () => ( +registerRenderer(EventType.CONNECTION_LOGIN, () => ( Logging in... )); -registerDispatcher(EventType.CONNECTION_FAILED, () => ( +registerRenderer(EventType.CONNECTION_FAILED, () => ( Connect failed. )); -registerDispatcher(EventType.CONNECTION_CONNECTED, (data,handlerId) => ( +registerRenderer(EventType.CONNECTION_CONNECTED, (data,handlerId) => ( )); -registerDispatcher(EventType.CONNECTION_VOICE_CONNECT, () => ( +registerRenderer(EventType.CONNECTION_VOICE_CONNECT, () => ( Connecting voice bridge. )); -registerDispatcher(EventType.CONNECTION_VOICE_CONNECT_SUCCEEDED, () => ( +registerRenderer(EventType.CONNECTION_VOICE_CONNECT_SUCCEEDED, () => ( Voice bridge successfully connected. )); -registerDispatcher(EventType.CONNECTION_VOICE_CONNECT_FAILED, (data) => ( +registerRenderer(EventType.CONNECTION_VOICE_CONNECT_FAILED, (data) => ( <>{data.reason} {data.reconnect_delay > 0 ? Yes : No} )); -registerDispatcher(EventType.CONNECTION_VOICE_DROPPED, () => ( +registerRenderer(EventType.CONNECTION_VOICE_DROPPED, () => ( Voice bridge has been dropped. Trying to reconnect. )); -registerDispatcher(EventType.ERROR_PERMISSION, data => ( +registerRenderer(EventType.ERROR_PERMISSION, data => (
<>{data.permission ? data.permission.name : unknown} @@ -113,7 +113,7 @@ registerDispatcher(EventType.ERROR_PERMISSION, data => (
)); -registerDispatcher(EventType.CLIENT_VIEW_ENTER, (data, handlerId) => { +registerRenderer(EventType.CLIENT_VIEW_ENTER, (data, handlerId) => { switch (data.reason) { case ViewReasonId.VREASON_USER_ACTION: 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) { case ViewReasonId.VREASON_USER_ACTION: 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) { case ViewReasonId.VREASON_MOVED: 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) { case ViewReasonId.VREASON_MOVED: 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) { case ViewReasonId.VREASON_USER_ACTION: 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) { case ViewReasonId.VREASON_USER_ACTION: return ( @@ -456,24 +456,24 @@ registerDispatcher(EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL, (data, handlerId) => ); 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) => ( + )); -registerDispatcher(EventType.SERVER_HOST_MESSAGE,data => ( - +registerRenderer(EventType.SERVER_HOST_MESSAGE,(data, handlerId) => ( + )); -registerDispatcher(EventType.SERVER_HOST_MESSAGE_DISCONNECT,data => ( - +registerRenderer(EventType.SERVER_HOST_MESSAGE_DISCONNECT,(data, handlerId) => ( + )); -registerDispatcher(EventType.CLIENT_NICKNAME_CHANGED,(data, handlerId) => ( +registerRenderer(EventType.CLIENT_NICKNAME_CHANGED,(data, handlerId) => ( <>{data.old_name} @@ -481,44 +481,44 @@ registerDispatcher(EventType.CLIENT_NICKNAME_CHANGED,(data, handlerId) => ( )); -registerDispatcher(EventType.CLIENT_NICKNAME_CHANGED_OWN,() => ( +registerRenderer(EventType.CLIENT_NICKNAME_CHANGED_OWN,() => ( Nickname successfully changed. )); -registerDispatcher(EventType.CLIENT_NICKNAME_CHANGE_FAILED,(data) => ( +registerRenderer(EventType.CLIENT_NICKNAME_CHANGE_FAILED,(data) => ( <>{data.reason} )); -registerDispatcher(EventType.GLOBAL_MESSAGE, (data, handlerId) => <> +registerRenderer(EventType.GLOBAL_MESSAGE, (data, handlerId) => <> - {data.message} + ); -registerDispatcher(EventType.DISCONNECTED,() => ( +registerRenderer(EventType.DISCONNECTED,() => ( Disconnected from server )); -registerDispatcher(EventType.RECONNECT_SCHEDULED,data => ( +registerRenderer(EventType.RECONNECT_SCHEDULED,data => ( <>{format_time(data.timeout, tr("now"))} )); -registerDispatcher(EventType.RECONNECT_CANCELED,() => ( +registerRenderer(EventType.RECONNECT_CANCELED,() => ( Reconnect canceled. )); -registerDispatcher(EventType.RECONNECT_CANCELED,() => ( +registerRenderer(EventType.RECONNECT_CANCELED,() => ( Reconnecting... )); -registerDispatcher(EventType.SERVER_BANNED,(data, handlerId) => { +registerRenderer(EventType.SERVER_BANNED,(data, handlerId) => { const time = data.time === 0 ? ever : <>{format_time(data.time * 1000, tr("one second"))}; const reason = data.message ? <> Reason: {data.message} : undefined; @@ -544,11 +544,11 @@ registerDispatcher(EventType.SERVER_BANNED,(data, handlerId) => { ); }); -registerDispatcher(EventType.SERVER_REQUIRES_PASSWORD,() => ( +registerRenderer(EventType.SERVER_REQUIRES_PASSWORD,() => ( Server requires a password to connect. )); -registerDispatcher(EventType.SERVER_CLOSED,data => { +registerRenderer(EventType.SERVER_CLOSED,data => { if(data.message) return ( @@ -558,7 +558,7 @@ registerDispatcher(EventType.SERVER_CLOSED,data => { return Server has been closed.; }); -registerDispatcher(EventType.CONNECTION_COMMAND_ERROR,data => { +registerRenderer(EventType.CONNECTION_COMMAND_ERROR,data => { let message; if(typeof data.error === "string") 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) { return ( @@ -593,13 +593,13 @@ registerDispatcher(EventType.CHANNEL_CREATE,(data, handlerId) => { } }); -registerDispatcher("channel.show",(data, handlerId) => ( +registerRenderer("channel.show",(data, handlerId) => ( )); -registerDispatcher(EventType.CHANNEL_DELETE,(data, handlerId) => { +registerRenderer(EventType.CHANNEL_DELETE,(data, handlerId) => { if(data.ownAction) { return ( @@ -616,24 +616,24 @@ registerDispatcher(EventType.CHANNEL_DELETE,(data, handlerId) => { } }); -registerDispatcher("channel.hide",(data, handlerId) => ( +registerRenderer("channel.hide",(data, handlerId) => ( )); -registerDispatcher(EventType.CLIENT_POKE_SEND,(data, handlerId) => ( +registerRenderer(EventType.CLIENT_POKE_SEND,(data, handlerId) => ( )); -registerDispatcher(EventType.CLIENT_POKE_RECEIVED,(data, handlerId) => { +registerRenderer(EventType.CLIENT_POKE_RECEIVED,(data, handlerId) => { if(data.message) { return ( - + ); } else { @@ -645,10 +645,10 @@ registerDispatcher(EventType.CLIENT_POKE_RECEIVED,(data, handlerId) => { } }); -registerDispatcher(EventType.PRIVATE_MESSAGE_RECEIVED, () => undefined); -registerDispatcher(EventType.PRIVATE_MESSAGE_SEND, () => undefined); +registerRenderer(EventType.PRIVATE_MESSAGE_RECEIVED, () => undefined); +registerRenderer(EventType.PRIVATE_MESSAGE_SEND, () => undefined); -registerDispatcher(EventType.WEBRTC_FATAL_ERROR, (data) => { +registerRenderer(EventType.WEBRTC_FATAL_ERROR, (data) => { if(data.retryTimeout) { let time = Math.ceil(data.retryTimeout / 1000); let minutes = Math.floor(time / 60); diff --git a/shared/js/ui/frames/log/ServerEventLog.tsx b/shared/js/ui/frames/log/ServerEventLog.tsx deleted file mode 100644 index fa594fc0..00000000 --- a/shared/js/ui/frames/log/ServerEventLog.tsx +++ /dev/null @@ -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; - private readonly listenerHandlerVisibilityChanged; - - private htmlTag: HTMLDivElement; - - private maxHistoryLength: number = 100; - - private eventLog: LogMessage[] = []; - - constructor(connection: ConnectionHandler) { - this.connection = connection; - this.uiEvents = new Registry(); - 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(, this.htmlTag); - - this.connection.events().on("notify_visibility_changed", this.listenerHandlerVisibilityChanged =event => { - if(event.visible) { - this.uiEvents.fire("notify_show"); - } - }); - } - - log(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(); - } -} \ No newline at end of file diff --git a/shared/js/ui/frames/side/AbstractConversationController.ts b/shared/js/ui/frames/side/AbstractConversationController.ts index bc9ffdaa..46d68371 100644 --- a/shared/js/ui/frames/side/AbstractConversationController.ts +++ b/shared/js/ui/frames/side/AbstractConversationController.ts @@ -29,8 +29,6 @@ export abstract class AbstractConversationController< protected currentSelectedConversation: ConversationType; protected currentSelectedListener: (() => void)[]; - protected crossChannelChatSupported = true; - protected constructor() { this.uiEvents = new Registry(); this.currentSelectedListener = []; @@ -68,6 +66,12 @@ export abstract class AbstractConversationController< 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_cross_conversation_support_changed", () => { + const currentConversation = this.getCurrentConversation(); + if(currentConversation) { + this.reportStateToUI(currentConversation); + } + })); } protected registerConversationEvents(conversation: ConversationType) { @@ -138,7 +142,7 @@ export abstract class AbstractConversationController< this.uiEvents.fire_react("notify_conversation_state", { chatId: conversation.getChatId(), state: "private", - crossChannelChatSupported: this.crossChannelChatSupported + crossChannelChatSupported: this.conversationManager.hasCrossConversationSupport() }); return; } @@ -154,7 +158,7 @@ export abstract class AbstractConversationController< chatFrameMaxMessageCount: kMaxChatFrameMessageSize, unreadTimestamp: conversation.getUnreadTimestamp(), - showUserSwitchEvents: conversation.isPrivate() || !this.crossChannelChatSupported, + showUserSwitchEvents: conversation.isPrivate() || !this.conversationManager.hasCrossConversationSupport(), sendEnabled: conversation.isSendEnabled(), events: [...conversation.getPresentEvents(), ...conversation.getPresentMessages()] @@ -251,18 +255,6 @@ export abstract class AbstractConversationController< 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("query_conversation_state") protected handleQueryConversationState(event: AbstractConversationUiEvents["query_conversation_state"]) { const conversation = this.conversationManager?.findConversationById(event.chatId); diff --git a/shared/js/ui/frames/side/AbstractConversationRenderer.scss b/shared/js/ui/frames/side/AbstractConversationRenderer.scss index 87dd94bf..0f631538 100644 --- a/shared/js/ui/frames/side/AbstractConversationRenderer.scss +++ b/shared/js/ui/frames/side/AbstractConversationRenderer.scss @@ -56,6 +56,9 @@ html:root { position: relative; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + .containerMessages { flex-grow: 1; flex-shrink: 1; diff --git a/shared/js/ui/frames/side/AbstractConversationRenderer.tsx b/shared/js/ui/frames/side/AbstractConversationRenderer.tsx index 79357c25..9b2afe07 100644 --- a/shared/js/ui/frames/side/AbstractConversationRenderer.tsx +++ b/shared/js/ui/frames/side/AbstractConversationRenderer.tsx @@ -26,9 +26,9 @@ import {ChatBox} from "tc-shared/ui/react-elements/ChatBox"; 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; } - return ; + return ; }); const ChatEventMessageRenderer = React.memo((props: { @@ -71,7 +71,7 @@ const ChatEventMessageRenderer = React.memo((props: {
{ /* Only for copy purposes */ }
- +

{ /* Only for copy purposes */ } diff --git a/shared/js/ui/frames/side/ChannelBarController.ts b/shared/js/ui/frames/side/ChannelBarController.ts new file mode 100644 index 00000000..c08f39f0 --- /dev/null +++ b/shared/js/ui/frames/side/ChannelBarController.ts @@ -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; + + private channelConversations: ChannelConversationController; + private description: ChannelDescriptionController; + + private currentConnection: ConnectionHandler; + private listenerConnection: (() => void)[]; + + private currentChannel: ChannelEntry; + private listenerChannel: (() => void)[]; + + constructor() { + this.uiEvents = new Registry(); + 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; + } + } +} \ No newline at end of file diff --git a/shared/js/ui/frames/side/ChannelBarDefinitions.ts b/shared/js/ui/frames/side/ChannelBarDefinitions.ts new file mode 100644 index 00000000..a14ffc18 --- /dev/null +++ b/shared/js/ui/frames/side/ChannelBarDefinitions.ts @@ -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, + }, + "description": { + events: Registry + }, + "file-transfer": { + /* TODO! */ + }, + "none": {} +} + +export type ChannelBarNotifyModeData = { + 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 +} \ No newline at end of file diff --git a/shared/js/ui/frames/side/ChannelBarRenderer.tsx b/shared/js/ui/frames/side/ChannelBarRenderer.tsx new file mode 100644 index 00000000..bfaf031f --- /dev/null +++ b/shared/js/ui/frames/side/ChannelBarRenderer.tsx @@ -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>(undefined); +const ChannelContext = React.createContext<{ channelId: number, handlerId: string }>(undefined); + +function useModeData(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(() => { + events.fire("query_mode"); + return "none"; + }, [ channelContext.channelId, channelContext.handlerId ]); + events.reactUse("notify_mode", event => setMode(event.mode)); + + switch (mode) { + case "conversation": + return ; + + case "description": + return ; + + 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 ( + + ); +}); + +const ModeRendererDescription = React.memo(() => { + const channelContext = useContext(ChannelContext); + const data = useModeData("description", [ channelContext ]); + if(!data) { return null; } + + return ( + + ); +}); + +export const ChannelBarRenderer = (props: { events: Registry }) => { + 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 ( + + + + + + ); +} \ No newline at end of file diff --git a/shared/js/ui/frames/side/ChannelConversationController.ts b/shared/js/ui/frames/side/ChannelConversationController.ts index 0ce07034..6f06cadc 100644 --- a/shared/js/ui/frames/side/ChannelConversationController.ts +++ b/shared/js/ui/frames/side/ChannelConversationController.ts @@ -11,7 +11,6 @@ import { ChannelConversationManager, ChannelConversationManagerEvents } from "tc-shared/conversations/ChannelConversationManager"; -import {ServerFeature} from "tc-shared/connection/ServerFeatures"; import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions"; export class ChannelConversationController extends AbstractConversationController< @@ -75,18 +74,6 @@ export class ChannelConversationController extends AbstractConversationControlle 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("action_delete_message") diff --git a/shared/js/ui/frames/side/ChannelDescriptionController.ts b/shared/js/ui/frames/side/ChannelDescriptionController.ts new file mode 100644 index 00000000..eb12436f --- /dev/null +++ b/shared/js/ui/frames/side/ChannelDescriptionController.ts @@ -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; + private currentChannel: ChannelEntry; + private listenerChannel: (() => void)[]; + + private descriptionSendPending = false; + private cachedDescriptionStatus: ChannelDescriptionStatus; + private cachedDescriptionAge: number; + + constructor() { + this.uiEvents = new Registry(); + 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((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(); + } +} \ No newline at end of file diff --git a/shared/js/ui/frames/side/ChannelDescriptionDefinitions.ts b/shared/js/ui/frames/side/ChannelDescriptionDefinitions.ts new file mode 100644 index 00000000..5b55c8fd --- /dev/null +++ b/shared/js/ui/frames/side/ChannelDescriptionDefinitions.ts @@ -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, + } +} \ No newline at end of file diff --git a/shared/js/ui/frames/side/ChannelDescriptionRenderer.scss b/shared/js/ui/frames/side/ChannelDescriptionRenderer.scss new file mode 100644 index 00000000..c582c4e0 --- /dev/null +++ b/shared/js/ui/frames/side/ChannelDescriptionRenderer.scss @@ -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%; + } +} \ No newline at end of file diff --git a/shared/js/ui/frames/side/ChannelDescriptionRenderer.tsx b/shared/js/ui/frames/side/ChannelDescriptionRenderer.tsx new file mode 100644 index 00000000..5afcb177 --- /dev/null +++ b/shared/js/ui/frames/side/ChannelDescriptionRenderer.tsx @@ -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 ( +
+
+ {props.children} +
+
+ ); +} + +const DescriptionRenderer = React.memo((props: { description: string, handlerId: string }) => { + if(!props.description) { + return ( + + Channel has no description + + ) + } + + return ( +
+ +
+ ) +}); + +const DescriptionErrorRenderer = React.memo((props: { error: string }) => ( + + An error happened while fetching the channel description:
+ {props.error} +
+)); + +const PermissionErrorRenderer = React.memo((props: { failedPermission: string }) => ( + + You don't have the permission to watch the channel description.  + {props.failedPermission} + +)); + +export const ChannelDescriptionRenderer = React.memo((props: { events: Registry }) => { + const [ description, setDescription ] = useState(() => { + props.events.fire("query_description"); + return { status: "loading" }; + }); + props.events.reactUse("notify_description", event => setDescription(event.status)); + + switch (description.status) { + case "success": + return ( + + ); + + case "error": + return ( + + ); + + case "no-permissions": + return ( + + ); + + case "loading": + default: + return ( + + loading channel description + + ); + } +}); \ No newline at end of file diff --git a/shared/js/ui/frames/side/HeaderController.ts b/shared/js/ui/frames/side/HeaderController.ts index 085137e4..6f8f0359 100644 --- a/shared/js/ui/frames/side/HeaderController.ts +++ b/shared/js/ui/frames/side/HeaderController.ts @@ -51,7 +51,7 @@ export class SideHeaderController { }); this.uiEvents.on("action_switch_channel_chat", () => { - this.connection.getSideBar().showChannelConversations(); + this.connection.getSideBar().showChannel(); }); this.uiEvents.on("action_bot_manage", () => { diff --git a/shared/js/ui/modal/settings/Notifications.tsx b/shared/js/ui/modal/settings/Notifications.tsx index 90b46370..c219c38f 100644 --- a/shared/js/ui/modal/settings/Notifications.tsx +++ b/shared/js/ui/modal/settings/Notifications.tsx @@ -3,15 +3,15 @@ import {useRef, useState} from "react"; import {Registry} from "tc-shared/events"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; 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 {Checkbox} from "tc-shared/ui/react-elements/Checkbox"; 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"); @@ -21,7 +21,7 @@ interface EventGroup { key: string; name: string; - events?: string[]; + events?: (keyof TypeInfo)[]; subgroups?: EventGroup[]; } @@ -340,23 +340,23 @@ const knownEventGroups: EventGroup[] = [ key: "client-messages", name: "Messages", events: [ - EventType.CLIENT_POKE_RECEIVED, - EventType.CLIENT_POKE_SEND, - EventType.PRIVATE_MESSAGE_SEND, - EventType.PRIVATE_MESSAGE_RECEIVED + "client.poke.received", + "client.poke.send", + "private.message.send", + "private.message.received" ] }, { key: "client-view", name: "View", events: [ - EventType.CLIENT_VIEW_ENTER, - EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL, - EventType.CLIENT_VIEW_MOVE, - EventType.CLIENT_VIEW_MOVE_OWN, - EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL, - EventType.CLIENT_VIEW_LEAVE, - EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL + "client.view.enter", + "client.view.enter.own.channel", + "client.view.move", + "client.view.move.own", + "client.view.move.own.channel", + "client.view.leave", + "client.view.leave.own.channel" ] } ] @@ -365,45 +365,45 @@ const knownEventGroups: EventGroup[] = [ key: "server", name: "Server", events: [ - EventType.GLOBAL_MESSAGE, - EventType.SERVER_CLOSED, - EventType.SERVER_BANNED, + "global.message", + "server.closed", + "server.banned", ] }, { key: "connection", name: "Connection", events: [ - EventType.CONNECTION_BEGIN, - EventType.CONNECTION_CONNECTED, - EventType.CONNECTION_FAILED + "connection.begin", + "connection.connected", + "connection.failed" ] } ]; -const groupNames: { [key: string]: string } = {}; -groupNames[EventType.CLIENT_POKE_RECEIVED] = tr("You received a poke"); -groupNames[EventType.CLIENT_POKE_SEND] = tr("You send a poke"); -groupNames[EventType.PRIVATE_MESSAGE_SEND] = tr("You received a private message"); -groupNames[EventType.PRIVATE_MESSAGE_RECEIVED] = tr("You send a private message"); +const groupNames: { [T in keyof TypeInfo]?: string } = {}; +groupNames["client.poke.received"] = tr("You received a poke"); +groupNames["client.poke.send"] = tr("You send a poke"); +groupNames["private.message.send"] = tr("You received 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[EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL] = tr("A client enters your view and your channel"); +groupNames["client.view.enter"] = tr("A client enters your view"); +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[EventType.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"] = tr("A client switches/gets moved/kicked"); +groupNames["client.view.move.own.channel"] = tr("A client switches/gets moved/kicked in to/out of your channel"); +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[EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL] = tr("A client leaves/disconnects of your channel"); +groupNames["client.view.leave"] = tr("A client leaves/disconnects of your view"); +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[EventType.SERVER_CLOSED] = tr("The server has been closed"); -groupNames[EventType.SERVER_BANNED] = tr("You've been banned from the server"); +groupNames["global.message"] = tr("A server message has been send"); +groupNames["server.closed"] = tr("The server has been closed"); +groupNames["server.banned"] = tr("You've been banned from the server"); -groupNames[EventType.CONNECTION_BEGIN] = tr("You're connecting to a server"); -groupNames[EventType.CONNECTION_CONNECTED] = tr("You've successfully connected to the server"); -groupNames[EventType.CONNECTION_FAILED] = tr("You're connect attempt failed"); +groupNames["connection.begin"] = tr("You're connecting to a server"); +groupNames["connection.connected"] = tr("You've successfully connected to the server"); +groupNames["connection.failed"] = tr("You're connect attempt failed"); function initializeController(events: Registry) { let filter = undefined; diff --git a/shared/js/ui/tree/Controller.tsx b/shared/js/ui/tree/Controller.tsx index 7516d71b..ac3dc990 100644 --- a/shared/js/ui/tree/Controller.tsx +++ b/shared/js/ui/tree/Controller.tsx @@ -258,6 +258,8 @@ class ChannelTreeController { this.sendChannelInfo(event.newChannel); this.sendChannelStatusIcon(event.newChannel); this.sendChannelTreeEntries(); + + this.sendClientTalkStatus(event.client); } @EventHandler("notify_selected_entry_changed") diff --git a/web/app/connection/ServerConnection.ts b/web/app/connection/ServerConnection.ts index 6e87d03f..be54869e 100644 --- a/web/app/connection/ServerConnection.ts +++ b/web/app/connection/ServerConnection.ts @@ -14,7 +14,6 @@ import * as log from "tc-shared/log"; import {LogCategory, logDebug, logError, logTrace} from "tc-shared/log"; import {Regex} from "tc-shared/ui/modal/ModalConnect"; 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 {AbstractVoiceConnection} from "tc-shared/connection/VoiceConnection"; 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 {RtpVideoConnection} from "tc-shared/connection/rtc/video/Connection"; import { tr } from "tc-shared/i18n/localize"; -import {createErrorModal} from "tc-shared/ui/elements/Modal"; class ReturnListener { resolve: (value?: T | PromiseLike) => void; @@ -286,7 +284,7 @@ export class ServerConnection extends AbstractServerConnection { private startHandshake() { this.updateConnectionState(ConnectionState.INITIALISING); - this.client.log.log(EventType.CONNECTION_LOGIN, {}); + this.client.log.log("connection.login", {}); this.handshakeHandler.initialize(); this.handshakeHandler.startHandshake(); } diff --git a/web/app/legacy/voice/VoiceHandler.ts b/web/app/legacy/voice/VoiceHandler.ts index 16cd6cfc..ad4279fa 100644 --- a/web/app/legacy/voice/VoiceHandler.ts +++ b/web/app/legacy/voice/VoiceHandler.ts @@ -15,7 +15,6 @@ import {ConnectionStatistics, ServerConnectionEvents} from "tc-shared/connection import {ConnectionState} from "tc-shared/ConnectionHandler"; import {VoiceBridge, VoicePacket, VoiceWhisperPacket} from "./bridge/VoiceBridge"; import {NativeWebRTCVoiceBridge} from "./bridge/NativeWebRTCVoiceBridge"; -import {EventType} from "tc-shared/ui/frames/log/Definitions"; import { kUnknownWhisperClientUniqueId, WhisperSession, @@ -191,7 +190,7 @@ export class VoiceConnection extends AbstractVoiceConnection { }, payload))) }; this.voiceBridge.callbackDisconnect = () => { - this.connection.client.log.log(EventType.CONNECTION_VOICE_DROPPED, { }); + this.connection.client.log.log("connection.voice.dropped", { }); if(!this.connectionLostModalOpen) { this.connectionLostModalOpen = true; 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.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.voiceBridge.connect().then(result => { if(result.type === "success") { this.lastConnectAttempt = 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; if(currentInput) { this.voiceBridge.setInput(currentInput).catch(error => { @@ -226,7 +225,7 @@ export class VoiceConnection extends AbstractVoiceConnection { let doReconnect = result.allowReconnect && this.connectAttemptCounter < 5; 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, reconnect_delay: doReconnect ? 1 : 0 });