diff --git a/ChangeLog.md b/ChangeLog.md index 213f605e..7577ec46 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,8 @@ # Changelog: +* **21.07.20** + - Added the enchanted server log system + - Recoded the server log with react + * **20.07.20** - Some general project cleanup - Heavily improved the IPC internal API diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index b48ddbe6..1616b15c 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -5,8 +5,6 @@ import {GroupManager} from "tc-shared/permission/GroupManager"; import {ServerSettings, Settings, settings, StaticSettings} from "tc-shared/settings"; import {Sound, SoundManager} from "tc-shared/sound/Sounds"; import {LocalClientEntry} from "tc-shared/ui/client"; -import * as server_log from "tc-shared/ui/frames/server_log"; -import {ServerLog} from "tc-shared/ui/frames/server_log"; import {ConnectionProfile, default_profile, find_profile} from "tc-shared/profiles/ConnectionProfile"; import {ServerAddress} from "tc-shared/ui/server"; import * as log from "tc-shared/log"; @@ -35,6 +33,8 @@ import {FileTransferState, TransferProvider} from "tc-shared/file/Transfer"; import {traj} from "tc-shared/i18n/localize"; import {md5} from "tc-shared/crypto/md5"; import {guid} from "tc-shared/crypto/uid"; +import {ServerEventLog} from "tc-shared/ui/frames/log/ServerEventLog"; +import {EventType} from "tc-shared/ui/frames/log/Definitions"; export enum DisconnectReason { HANDLER_DESTROYED, @@ -172,7 +172,7 @@ export class ConnectionHandler { }; invoke_resized_on_activate: boolean = false; - log: ServerLog; + log: ServerEventLog; constructor() { this.handlerId = guid(); @@ -188,7 +188,7 @@ export class ConnectionHandler { this.fileManager = new FileManager(this); this.permissions = new PermissionManager(this); - this.log = new ServerLog(this); + this.log = new ServerEventLog(this); this.side_bar = new Frame(this); this.sound = new SoundManager(this); this.hostbanner = new Hostbanner(this); @@ -261,7 +261,7 @@ export class ConnectionHandler { } } log.info(LogCategory.CLIENT, tr("Start connection to %s:%d"), server_address.host, server_address.port); - this.log.log(server_log.Type.CONNECTION_BEGIN, { + this.log.log(EventType.CONNECTION_BEGIN, { address: { server_hostname: server_address.host, server_port: server_address.port @@ -294,7 +294,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(server_log.Type.CONNECTION_HOSTNAME_RESOLVE, {}); + this.log.log(EventType.CONNECTION_HOSTNAME_RESOLVE, {}); try { const resolved = await dns.resolve_address(server_address, { timeout: 5000 }) || {} as any; if(id != this._connect_initialize_id) @@ -302,7 +302,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(server_log.Type.CONNECTION_HOSTNAME_RESOLVED, { + this.log.log(EventType.CONNECTION_HOSTNAME_RESOLVED, { address: { server_port: server_address.port, server_hostname: server_address.host @@ -339,7 +339,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(server_log.Type.DISCONNECTED, {}); + this.log.log(EventType.DISCONNECTED, {}); } getClient() : LocalClientEntry { return this._local_client; } @@ -362,7 +362,12 @@ export class ConnectionHandler { this.connection_state = event.new_state; log.info(LogCategory.CLIENT, tr("Client connected")); - this.log.log(server_log.Type.CONNECTION_CONNECTED, { + this.log.log(EventType.CONNECTION_CONNECTED, { + serverAddress: { + server_port: this.channelTree.server.remote_address.port, + server_hostname: this.channelTree.server.remote_address.host + }, + serverName: this.channelTree.server.properties.virtualserver_name, own_client: this.getClient().log_data() }); this.sound.play(Sound.CONNECTION_CONNECTED); @@ -484,12 +489,12 @@ export class ConnectionHandler { case DisconnectReason.HANDLER_DESTROYED: if(data) { this.sound.play(Sound.CONNECTION_DISCONNECTED); - this.log.log(server_log.Type.DISCONNECTED, {}); + this.log.log(EventType.DISCONNECTED, {}); } break; case DisconnectReason.DNS_FAILED: log.error(LogCategory.CLIENT, tr("Failed to resolve hostname: %o"), data); - this.log.log(server_log.Type.CONNECTION_HOSTNAME_RESOLVE_ERROR, { + this.log.log(EventType.CONNECTION_HOSTNAME_RESOLVE_ERROR, { message: data as any }); this.sound.play(Sound.CONNECTION_REFUSED); @@ -497,7 +502,10 @@ export class ConnectionHandler { case DisconnectReason.CONNECT_FAILURE: if(this._reconnect_attempt) { auto_reconnect = true; - this.log.log(server_log.Type.CONNECTION_FAILED, {}); + this.log.log(EventType.CONNECTION_FAILED, { serverAddress: { + server_port: this.channelTree.server.remote_address.port, + server_hostname: this.channelTree.server.remote_address.host + } }); break; } if(data) @@ -572,7 +580,7 @@ export class ConnectionHandler { break; case DisconnectReason.SERVER_CLOSED: - this.log.log(server_log.Type.SERVER_CLOSED, {message: data.reasonmsg}); + this.log.log(EventType.SERVER_CLOSED, {message: data.reasonmsg}); createErrorModal( tr("Server closed"), @@ -584,7 +592,7 @@ export class ConnectionHandler { auto_reconnect = true; break; case DisconnectReason.SERVER_REQUIRES_PASSWORD: - this.log.log(server_log.Type.SERVER_REQUIRES_PASSWORD, {}); + this.log.log(EventType.SERVER_REQUIRES_PASSWORD, {}); createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => { if(!(typeof password === "string")) return; @@ -625,7 +633,7 @@ export class ConnectionHandler { this.sound.play(Sound.CONNECTION_BANNED); break; case DisconnectReason.CLIENT_BANNED: - this.log.log(server_log.Type.SERVER_BANNED, { + this.log.log(EventType.SERVER_BANNED, { invoker: { client_name: data["invokername"], client_id: parseInt(data["invokerid"]), @@ -655,7 +663,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(server_log.Type.RECONNECT_SCHEDULED, {timeout: 5000}); + this.log.log(EventType.RECONNECT_SCHEDULED, {timeout: 5000}); log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect. Reconnecting in 5000ms")); const server_address = this.serverConnection.remote_address(); @@ -663,7 +671,7 @@ export class ConnectionHandler { this._reconnect_timer = setTimeout(() => { this._reconnect_timer = undefined; - this.log.log(server_log.Type.RECONNECT_EXECUTE, {}); + this.log.log(EventType.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})); @@ -675,7 +683,7 @@ export class ConnectionHandler { cancel_reconnect(log_event: boolean) { if(this._reconnect_timer) { - if(log_event) this.log.log(server_log.Type.RECONNECT_CANCELED, {}); + if(log_event) this.log.log(EventType.RECONNECT_CANCELED, {}); clearTimeout(this._reconnect_timer); this._reconnect_timer = undefined; } @@ -734,7 +742,7 @@ export class ConnectionHandler { if(Object.keys(property_update).length > 0) { this.serverConnection.send_command("clientupdate", property_update).catch(error => { log.warn(LogCategory.GENERAL, tr("Failed to update client audio hardware properties. Error: %o"), error); - this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to update audio hardware properties.")}); + this.log.log(EventType.ERROR_CUSTOM, {message: tr("Failed to update audio hardware properties.")}); /* Update these properties anyways (for case the server fails to handle the command) */ const updates = []; @@ -816,7 +824,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(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to sync handler state with server.")}); + this.log.log(EventType.ERROR_CUSTOM, {message: tr("Failed to sync handler state with server.")}); }); } @@ -1039,7 +1047,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(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to update away status.")}); + this.log.log(EventType.ERROR_CUSTOM, {message: tr("Failed to update away status.")}); }); this.event_registry.fire("notify_state_updated", { diff --git a/shared/js/connection/CommandHandler.ts b/shared/js/connection/CommandHandler.ts index 74762864..9dcfa71d 100644 --- a/shared/js/connection/CommandHandler.ts +++ b/shared/js/connection/CommandHandler.ts @@ -1,6 +1,5 @@ import * as log from "tc-shared/log"; import {LogCategory} from "tc-shared/log"; -import * as server_log from "tc-shared/ui/frames/server_log"; import {AbstractServerConnection, CommandOptions, ServerCommand} from "tc-shared/connection/ConnectionBase"; import {Sound} from "tc-shared/sound/Sounds"; import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; @@ -23,6 +22,7 @@ import {batch_updates, BatchUpdateType, flush_batched_updates} from "tc-shared/u import {OutOfViewClient} from "tc-shared/ui/frames/side/PrivateConversationManager"; import {renderBBCodeAsJQuery} from "tc-shared/text/bbcode"; import {tr} from "tc-shared/i18n/localize"; +import {EventClient, EventType} from "tc-shared/ui/frames/log/Definitions"; export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss { constructor(connection: AbstractServerConnection) { @@ -85,7 +85,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { this["notifyplaylistsongloaded"] = this.handleNotifyPlaylistSongLoaded; } - private loggable_invoker(unique_id, client_id, name) : server_log.base.Client | undefined { + private loggable_invoker(unique_id, client_id, name) : EventClient | undefined { const id = parseInt(client_id); if(typeof(client_id) === "undefined" || Number.isNaN(id)) return undefined; @@ -116,18 +116,18 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { if(res.id == ErrorID.PERMISSION_ERROR) { //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(server_log.Type.ERROR_PERMISSION, { + this.connection_handler.log.log(EventType.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 != ErrorID.EMPTY_RESULT) { - this.connection_handler.log.log(server_log.Type.ERROR_CUSTOM, { + this.connection_handler.log.log(EventType.ERROR_CUSTOM, { message: res.extra_message.length == 0 ? res.message : res.extra_message }); } } } else if(typeof(ex) === "string") { - this.connection_handler.log.log(server_log.Type.CONNECTION_COMMAND_ERROR, {error: ex}); + this.connection_handler.log.log(EventType.CONNECTION_COMMAND_ERROR, {error: ex}); } else { log.error(LogCategory.NETWORKING, tr("Invalid promise result type: %s. Result: %o"), typeof (ex), ex); } @@ -212,7 +212,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { if(properties.virtualserver_hostmessage_mode == 1) { /* show in log */ if(properties.virtualserver_hostmessage) - this.connection_handler.log.log(server_log.Type.SERVER_HOST_MESSAGE, { + this.connection_handler.log.log(EventType.SERVER_HOST_MESSAGE, { message: properties.virtualserver_hostmessage }); } else { @@ -227,7 +227,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(server_log.Type.SERVER_HOST_MESSAGE_DISCONNECT, { + this.connection_handler.log.log(EventType.SERVER_HOST_MESSAGE_DISCONNECT, { message: properties.virtualserver_welcomemessage }); @@ -241,7 +241,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { /* welcome message */ if(properties.virtualserver_welcomemessage) { - this.connection_handler.log.log(server_log.Type.SERVER_WELCOME_MESSAGE, { + this.connection_handler.log.log(EventType.SERVER_WELCOME_MESSAGE, { message: properties.virtualserver_welcomemessage }); } @@ -452,14 +452,13 @@ 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(server_log.Type.CLIENT_VIEW_ENTER, { + this.connection_handler.log.log(channel == own_channel ? EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL : EventType.CLIENT_VIEW_ENTER, { channel_from: old_channel ? old_channel.log_data() : undefined, channel_to: channel ? channel.log_data() : undefined, client: client.log_data(), invoker: this.loggable_invoker(invokeruid, invokerid, invokername), message:reason_msg, reason: parseInt(reason_id), - own_channel: channel == own_channel }); if(reason_id == ViewReasonId.VREASON_USER_ACTION) { @@ -543,7 +542,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(server_log.Type.CLIENT_VIEW_LEAVE, { + this.connection_handler.log.log(is_own_channel ? EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL : EventType.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(), @@ -551,7 +550,6 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { message: entry["reasonmsg"], reason: parseInt(entry["reasonid"]), ban_time: parseInt(entry["bantime"]), - own_channel: is_own_channel }); if(is_own_channel) { @@ -629,7 +627,8 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { } const own_channel = this.connection.client.getClient().currentChannel(); - this.connection_handler.log.log(server_log.Type.CLIENT_VIEW_MOVE, { + const event = self ? EventType.CLIENT_VIEW_MOVE_OWN : (channel_from == own_channel || channel_to == own_channel ? EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL : EventType.CLIENT_VIEW_MOVE); + this.connection_handler.log.log(event, { channel_from: channel_from ? { channel_id: channel_from.channelId, channel_name: channel_from.channelName() @@ -770,8 +769,24 @@ 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, { + message: json["msg"], + sender: { + client_unique_id: json["invokeruid"], + client_name: json["invokername"], + client_id: parseInt(json["invokerid"]) + } + }); } else { this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5}); + this.connection_handler.log.log(EventType.PRIVATE_MESSAGE_SEND, { + message: json["msg"], + target: { + client_unique_id: json["invokeruid"], + client_name: json["invokername"], + client_id: parseInt(json["invokerid"]) + } + }); } this.connection_handler.side_bar.info_frame().update_chat_counter(); } else if(mode == 2) { @@ -798,7 +813,11 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { message: json["msg"] }, invoker instanceof LocalClientEntry); } else if(mode == 3) { - this.connection_handler.log.log(server_log.Type.GLOBAL_MESSAGE, { + const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); + const conversations = this.connection_handler.side_bar.channel_conversations(); + + this.connection_handler.log.log(EventType.GLOBAL_MESSAGE, { + isOwnMessage: invoker instanceof LocalClientEntry, message: json["msg"], sender: { client_unique_id: json["invokeruid"], @@ -807,9 +826,6 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { } }); - const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"])); - const conversations = this.connection_handler.side_bar.channel_conversations(); - if(!(invoker instanceof LocalClientEntry)) this.connection_handler.channelTree.server.setUnread(true); @@ -921,6 +937,10 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { unique_id: json["invokeruid"] }, json["msg"]); + this.connection_handler.log.log(EventType.CLIENT_POKE_RECEIVED, { + sender: this.loggable_invoker(json["invokeruid"], json["invokerid"], json["invokername"]), + message: json["msg"] + }); this.connection_handler.sound.play(Sound.USER_POKED_SELF); } diff --git a/shared/js/devel_main.ts b/shared/js/devel_main.ts index 5b63b758..a01a3747 100644 --- a/shared/js/devel_main.ts +++ b/shared/js/devel_main.ts @@ -23,6 +23,17 @@ loader.register_task(Stage.LOADED, { priority: 10, function: async () => { console.error("Spawning video popup"); - spawnVideoPopout(); + //spawnVideoPopout(); + + Notification.requestPermission().then(permission => { + if(permission === "denied") + return; + + const notification = new Notification("Hello World", { + body: "This is a simple test notification - " + Math.random(), + renotify: true, + tag: "xx" + }); + }) } }); \ No newline at end of file diff --git a/shared/js/events/GlobalEvents.ts b/shared/js/events/GlobalEvents.ts index 5c2a2215..98968c8f 100644 --- a/shared/js/events/GlobalEvents.ts +++ b/shared/js/events/GlobalEvents.ts @@ -4,7 +4,15 @@ import {Registry} from "tc-shared/events"; export interface ClientGlobalControlEvents { /* open a basic window */ action_open_window: { - window: "bookmark-manage" | "query-manage" | "query-create" | "ban-list" | "permissions" | "token-list" | "token-use" | "settings", + window: + "bookmark-manage" | + "query-manage" | + "query-create" | + "ban-list" | + "permissions" | + "token-list" | + "token-use" | + "settings", connection?: ConnectionHandler }, diff --git a/shared/js/settings.ts b/shared/js/settings.ts index cf370329..2eb664bc 100644 --- a/shared/js/settings.ts +++ b/shared/js/settings.ts @@ -536,6 +536,13 @@ export class Settings extends StaticSettings { } }; + static readonly FN_EVENTS_NOTIFICATION_ENABLED: (event: string) => SettingsKey = event => { + return { + key: "notification_" + event + "_enabled", + valueType: "boolean" + } + }; + static readonly KEYS = (() => { const result = []; diff --git a/shared/js/text/bbcode.tsx b/shared/js/text/bbcode.tsx index cf36517c..e24e4941 100644 --- a/shared/js/text/bbcode.tsx +++ b/shared/js/text/bbcode.tsx @@ -1,6 +1,6 @@ import {XBBCodeRenderer} from "vendor/xbbcode/react"; import * as React from "react"; -import {rendererHTML, rendererReact} from "tc-shared/text/bbcode/renderer"; +import {rendererHTML, rendererReact, rendererText} from "tc-shared/text/bbcode/renderer"; import {parse as parseBBCode} from "vendor/xbbcode/parser"; import {fixupJQueryUrlTags} from "tc-shared/text/bbcode/url"; import {fixupJQueryImageTags} from "tc-shared/text/bbcode/image"; @@ -100,4 +100,8 @@ export function renderBBCodeAsJQuery(message: string, settings: BBCodeRenderOpti fixupJQueryImageTags(container); return [container.contents() as JQuery]; +} + +export function renderBBCodeAsText(message: string) { + return parseBBCode(message, { tag_whitelist: allowedBBCodes }).map(e => rendererText.render(e)).join(""); } \ No newline at end of file diff --git a/shared/js/ui/channel.ts b/shared/js/ui/channel.ts index 0bd0cf23..6d8b9694 100644 --- a/shared/js/ui/channel.ts +++ b/shared/js/ui/channel.ts @@ -11,7 +11,6 @@ import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/ import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import * as htmltags from "./htmltags"; import {hashPassword} from "tc-shared/utils/helpers"; -import * as server_log from "tc-shared/ui/frames/server_log"; import {openChannelInfo} from "tc-shared/ui/modal/ModalChannelInfo"; import {createChannelModal} from "tc-shared/ui/modal/ModalCreateChannel"; import {formatMessage} from "tc-shared/ui/frames/chat"; @@ -22,6 +21,7 @@ import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry"; import {ChannelEntryView as ChannelEntryView} from "./tree/Channel"; import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTransfer"; import {ViewReasonId} from "tc-shared/ConnectionHandler"; +import {EventChannelData} from "tc-shared/ui/frames/log/Definitions"; export enum ChannelType { PERMANENT, @@ -409,7 +409,8 @@ export class ChannelEntry extends ChannelTreeEntry { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-channel_switch", name: bold(tr("Switch to channel")), - callback: () => this.joinChannel() + callback: () => this.joinChannel(), + visible: this !== this.channelTree.client.getClient()?.currentChannel() },{ type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-filetransfer", @@ -766,7 +767,7 @@ export class ChannelEntry extends ChannelTreeEntry { this.channelTree.client.settings.changeServer(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this.channelId), mode); } - log_data() : server_log.base.Channel { + log_data() : EventChannelData { return { channel_name: this.channelName(), channel_id: this.channelId diff --git a/shared/js/ui/client.ts b/shared/js/ui/client.ts index 7482f1a5..58092d48 100644 --- a/shared/js/ui/client.ts +++ b/shared/js/ui/client.ts @@ -9,7 +9,6 @@ import {Group, GroupManager, GroupTarget, GroupType} from "tc-shared/permission/ import PermissionType from "tc-shared/permission/PermissionType"; import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal"; import * as htmltags from "tc-shared/ui/htmltags"; -import * as server_log from "tc-shared/ui/frames/server_log"; import {CommandResult, PlaylistSong} from "tc-shared/connection/ServerConnectionDeclaration"; import {ChannelEntry} from "tc-shared/ui/channel"; import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler"; @@ -27,6 +26,7 @@ import * as React from "react"; import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry"; import {spawnClientVolumeChange, spawnMusicBotVolumeChange} from "tc-shared/ui/modal/ModalChangeVolumeNew"; import {spawnPermissionEditorModal} from "tc-shared/ui/modal/permission/ModalPermissionEditor"; +import {EventClient, EventType} from "tc-shared/ui/frames/log/Definitions"; export enum ClientType { CLIENT_VOICE, @@ -535,13 +535,15 @@ export class ClientEntry extends ChannelTreeEntry { callback: () => { createInputModal(tr("Poke client"), tr("Poke message:
"), text => true, result => { if(typeof(result) === "string") { - //TODO tr - console.log("Poking client " + this.clientNickName() + " with message " + result); this.channelTree.client.serverConnection.send_command("clientpoke", { clid: this.clientId(), msg: result + }).then(() => { + this.channelTree.client.log.log(EventType.CLIENT_POKE_SEND, { + target: this.log_data(), + message: result + }); }); - } }, { width: 400, maxLength: 512 }).open(); } @@ -742,8 +744,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(server_log.Type.CLIENT_NICKNAME_CHANGED, { - own_client: false, + this.channelTree.client.log.log(EventType.CLIENT_NICKNAME_CHANGED, { client: this.log_data(), new_name: variable.value, old_name: old_value @@ -873,7 +874,7 @@ export class ClientEntry extends ChannelTreeEntry { } } - log_data() : server_log.base.Client { + log_data() : EventClient { return { client_unique_id: this.properties.client_unique_identifier, client_name: this.clientNickName(), @@ -980,16 +981,15 @@ export class LocalClientEntry extends ClientEntry { this.updateVariables({ key: "client_nickname", value: new_name }); /* change it locally */ return this.handle.serverConnection.command_helper.updateClient("client_nickname", new_name).then((e) => { settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, new_name); - this.channelTree.client.log.log(server_log.Type.CLIENT_NICKNAME_CHANGED, { + this.channelTree.client.log.log(EventType.CLIENT_NICKNAME_CHANGED_OWN, { client: this.log_data(), old_name: old_name, new_name: new_name, - own_client: true }); return true; }).catch((e: CommandResult) => { this.updateVariables({ key: "client_nickname", value: old_name }); /* change it back */ - this.channelTree.client.log.log(server_log.Type.CLIENT_NICKNAME_CHANGE_FAILED, { + this.channelTree.client.log.log(EventType.CLIENT_NICKNAME_CHANGE_FAILED, { reason: e.extra_message }); return false; diff --git a/shared/js/ui/frames/chat.ts b/shared/js/ui/frames/chat.ts index 0f23f9df..a2920b04 100644 --- a/shared/js/ui/frames/chat.ts +++ b/shared/js/ui/frames/chat.ts @@ -95,8 +95,13 @@ export function formatMessage(pattern: string, ...objects: any[]) : JQuery[] { } export function formatMessageString(pattern: string, ...args: string[]) : string { + return parseMessageWithArguments(pattern, args.length).map(e => typeof e === "string" ? e : args[e]).join(""); +} + +export function parseMessageWithArguments(pattern: string, argumentCount: number) : (string | number)[] { let begin = 0, found = 0; + let unspecifiedIndex = 0; let result: string[] = []; do { found = pattern.indexOf('{', found); @@ -111,27 +116,42 @@ export function formatMessageString(pattern: string, ...args: string[]) : string continue; } - result.push(pattern.substr(begin, found - begin)); //Append the text + result.push(pattern.substring(begin, found)); //Append the text let offset = 0; let number; - while ("0123456789".includes(pattern[found + 1 + offset])) offset++; - number = parseInt(offset > 0 ? pattern.substr(found + 1, offset) : "0"); + while ("0123456789".includes(pattern[found + 1 + offset])) + offset++; + + if(offset > 0) { + number = parseInt(pattern.substr(found + 1, offset)); + } else { + number = unspecifiedIndex++; + } + if(pattern[found + offset + 1] != '}') { found++; continue; } - if(args.length < number) + if(argumentCount < number) { log.warn(LogCategory.GENERAL, tr("Message to format contains invalid index (%o)"), number); - - result.push(args[number]); + result.push("{" + offset.toString() + "}"); + } else { + result.push(number); + } found = found + 1 + offset; begin = found + 1; } while(found++); - return result.join(""); + return result.reduce((prev, element) => { + if(typeof element === "string" && typeof prev.last() === "string") + prev.push(prev.pop() + element); + else + prev.push(element); + return prev; + }, []); } export namespace network { diff --git a/shared/js/ui/frames/connection_handlers.ts b/shared/js/ui/frames/connection_handlers.ts index 58c944e4..7dfe8b24 100644 --- a/shared/js/ui/frames/connection_handlers.ts +++ b/shared/js/ui/frames/connection_handlers.ts @@ -112,7 +112,7 @@ export class ConnectionManager { this._container_hostbanner.append(handler.hostbanner.html_tag); this._container_channel_tree.append(handler.channelTree.tag_tree()); this._container_chat.append(handler.side_bar.html_tag()); - this._container_log_server.append(handler.log.html_tag()); + this._container_log_server.append(handler.log.getHTMLTag()); if(handler.invoke_resized_on_activate) handler.resize_elements(); diff --git a/shared/js/ui/frames/log/Definitions.ts b/shared/js/ui/frames/log/Definitions.ts new file mode 100644 index 00000000..6c1eade1 --- /dev/null +++ b/shared/js/ui/frames/log/Definitions.ts @@ -0,0 +1,321 @@ +import {PermissionInfo} from "tc-shared/permission/PermissionManager"; +import {ViewReasonId} from "tc-shared/ConnectionHandler"; +import * as React from "react"; +import {ServerEventLog} from "tc-shared/ui/frames/log/ServerEventLog"; + +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_SETUP_FAILED = "connection.voice.setup.failed", + 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", + + CHANNEL_CREATE_OWN = "channel.create.own", + CHANNEL_DELETE_OWN = "channel.delete.own", + + 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" +} + +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; + } + + export type EventChannelDelete = { + deleter: EventClient; + channel: EventChannelData; + } + + 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 EventConnectionVoiceSetupFailed = { + reason: string; + reconnect_delay: number; /* if less or equal to 0 reconnect is prohibited */ + } + + 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 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.setup.failed": event.EventConnectionVoiceSetupFailed; + "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.delete": event.EventChannelDelete; + + "channel.create.own": event.EventChannelCreate; + "channel.delete.own": event.EventChannelDelete; + + "client.poke.received": event.EventClientPokeReceived, + "client.poke.send": event.EventClientPokeSend, + + + "private.message.received": event.EventPrivateMessageReceived, + "private.message.send": event.EventPrivateMessageSend, + + "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 + } +} \ No newline at end of file diff --git a/shared/js/ui/frames/log/DispatcherLog.scss b/shared/js/ui/frames/log/DispatcherLog.scss new file mode 100644 index 00000000..ac0b0014 --- /dev/null +++ b/shared/js/ui/frames/log/DispatcherLog.scss @@ -0,0 +1,5 @@ +.clientEntry, .channelEntry { + color: var(--server-log-tree-entry); + font-weight: 700; + cursor: pointer; +} \ No newline at end of file diff --git a/shared/js/ui/frames/log/DispatcherLog.tsx b/shared/js/ui/frames/log/DispatcherLog.tsx new file mode 100644 index 00000000..aa310f08 --- /dev/null +++ b/shared/js/ui/frames/log/DispatcherLog.tsx @@ -0,0 +1,607 @@ +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"; +import {BBCodeRenderer} from "tc-shared/text/bbcode"; +import {format_time} from "tc-shared/ui/frames/chat"; +import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; + +const cssStyle = require("./DispatcherLog.scss"); +const cssStyleRenderer = require("./Renderer.scss"); + +export type DispatcherLog = (data: TypeInfo[T], handlerId: string, eventType: T) => React.ReactNode; + +const dispatchers: {[key: string]: DispatcherLog} = { }; +function registerDispatcher(key: T, builder: DispatcherLog) { + dispatchers[key] = builder; +} + +export function findLogDispatcher(type: T) : DispatcherLog { + return dispatchers[type]; +} + +export function getRegisteredLogDispatchers() : TypeInfo[] { + return Object.keys(dispatchers) as any; +} + +/* TODO: Enable context menu */ +const ClientRenderer = (props: { client: EventClient, handlerId: string, braces?: boolean }) => ( +
+ {props.client.client_name} +
+); + +/* TODO: Enable context menu */ +const ChannelRenderer = (props: { channel: EventChannelData, handlerId: string, braces?: boolean }) => ( +
+ {props.channel.channel_name} +
+); + +registerDispatcher(EventType.ERROR_CUSTOM, data =>
{data.message}
); + +registerDispatcher(EventType.CONNECTION_BEGIN, data => ( + + <>{data.address.server_hostname} + <>{data.address.server_port == 9987 ? "" : (":" + data.address.server_port)} + +)); + +registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVE, () => ( + Resolving hostname +)); + +registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVED, data => ( + + <>{data.address.server_hostname} + <>{data.address.server_port} + +)); + +registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVE_ERROR, data => ( + + <>{data.message} + +)); + +registerDispatcher(EventType.CONNECTION_LOGIN, () => ( + Logging in... +)); + +registerDispatcher(EventType.CONNECTION_FAILED, () => ( + Connect failed. +)); + +registerDispatcher(EventType.CONNECTION_CONNECTED, (data,handlerId) => ( + + + +)); + +registerDispatcher(EventType.CONNECTION_VOICE_SETUP_FAILED, (data) => ( + + <>{data.reason} + {data.reconnect_delay > 0 ? Yes : No} + +)); + +registerDispatcher(EventType.ERROR_PERMISSION, data => ( +
+ + <>{data.permission ? data.permission.name : unknown} + +
+)); + +registerDispatcher(EventType.CLIENT_VIEW_ENTER, (data, handlerId) => { + switch (data.reason) { + case ViewReasonId.VREASON_USER_ACTION: + if(data.channel_from) { + return ( + + + + + + ); + } else { + return ( + + + + + ); + } + + case ViewReasonId.VREASON_MOVED: + if(data.channel_from) { + return ( + + + + + + + ); + } else { + return ( + + + + + + ); + } + + case ViewReasonId.VREASON_CHANNEL_KICK: + if(data.channel_from) { + return ( + + + + + + <>{data.message ? " (" + data.message + ")" : ""} + + ); + } else { + return ( + + + + + <>{data.message ? " (" + data.message + ")" : ""} + + ); + } + + case ViewReasonId.VREASON_SYSTEM: + return undefined; + + default: + return ( +
+ + <>{data.reason} + +
+ ); + } +}); + +registerDispatcher(EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL, (data, handlerId) => { + switch (data.reason) { + case ViewReasonId.VREASON_USER_ACTION: + if(data.channel_from) { + return ( + + + + + + ); + } else { + return ( + + + + + ); + } + + case ViewReasonId.VREASON_MOVED: + if(data.channel_from) { + return ( + + + + + + + ); + } else { + return ( + + + + + + ); + } + + case ViewReasonId.VREASON_CHANNEL_KICK: + if(data.channel_from) { + return ( + + + + + + <>{data.message ? " (" + data.message + ")" : ""} + + ); + } else { + return ( + + + + + <>{data.message ? " (" + data.message + ")" : ""} + + ); + } + + case ViewReasonId.VREASON_SYSTEM: + return undefined; + + default: + return ( +
+ + <>{data.reason} + +
+ ); + } +}); + +registerDispatcher(EventType.CLIENT_VIEW_MOVE, (data, handlerId) => { + switch (data.reason) { + case ViewReasonId.VREASON_MOVED: + return ( + + + + + + + ); + + case ViewReasonId.VREASON_USER_ACTION: + return ( + + + + + + ); + + case ViewReasonId.VREASON_CHANNEL_KICK: + return ( + + + + + + <>{data.message ? " (" + data.message + ")" : ""} + + ); + + default: + return ( +
+ + <>{data.reason} + +
+ ); + } +}); + +registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL, findLogDispatcher(EventType.CLIENT_VIEW_MOVE)); + +registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN, (data, handlerId) => { + switch (data.reason) { + case ViewReasonId.VREASON_MOVED: + return ( + + + + + + + ); + + case ViewReasonId.VREASON_USER_ACTION: + return ( + + + + + + ); + + case ViewReasonId.VREASON_CHANNEL_KICK: + return ( + + + + + + <>{data.message ? " (" + data.message + ")" : ""} + + ); + + default: + return ( +
+ + <>{data.reason} + +
+ ); + } +}); + +registerDispatcher(EventType.CLIENT_VIEW_LEAVE, (data, handlerId) => { + switch (data.reason) { + case ViewReasonId.VREASON_USER_ACTION: + return ( + + + + + + ); + + case ViewReasonId.VREASON_SERVER_LEFT: + return ( + + + <>{data.message ? " (" + data.message + ")" : ""} + + ); + + case ViewReasonId.VREASON_SERVER_KICK: + return ( + + + + <>{data.message ? " (" + data.message + ")" : ""} + + ); + + case ViewReasonId.VREASON_CHANNEL_KICK: + return ( + + + + + <>{data.message ? " (" + data.message + ")" : ""} + + ); + + case ViewReasonId.VREASON_BAN: + let duration = permanently; + if(data.ban_time) + duration = <>{" " + formatDate(data.ban_time)}; + + return ( + + + {duration} + + <>{data.message ? " (" + data.message + ")" : ""} + + ); + + case ViewReasonId.VREASON_TIMEOUT: + return ( + + + <>{data.message ? " (" + data.message + ")" : ""} + + ); + + case ViewReasonId.VREASON_MOVED: + return ( + + + + + + + ); + + default: + return ( +
+ + <>{data.reason} + +
+ ); + } +}); + +registerDispatcher(EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL, (data, handlerId) => { + switch (data.reason) { + case ViewReasonId.VREASON_USER_ACTION: + return ( + + + + + + ); + + case ViewReasonId.VREASON_MOVED: + return ( + + + + + + + ); + + default: + return findLogDispatcher(EventType.CLIENT_VIEW_LEAVE)(data, handlerId, EventType.CLIENT_VIEW_LEAVE); + } +}); + +registerDispatcher(EventType.SERVER_WELCOME_MESSAGE,data => ( + +)); + +registerDispatcher(EventType.SERVER_HOST_MESSAGE,data => ( + +)); + +registerDispatcher(EventType.SERVER_HOST_MESSAGE_DISCONNECT,data => ( + +)); + + +registerDispatcher(EventType.CLIENT_NICKNAME_CHANGED,(data, handlerId) => ( + + + <>{data.old_name} + <>{data.new_name} + +)); + +registerDispatcher(EventType.CLIENT_NICKNAME_CHANGED_OWN,() => ( + Nickname successfully changed. +)); + +registerDispatcher(EventType.CLIENT_NICKNAME_CHANGE_FAILED,(data) => ( + + <>{data.reason} + +)); + +registerDispatcher(EventType.GLOBAL_MESSAGE, () => undefined); + + +registerDispatcher(EventType.DISCONNECTED,() => ( + Disconnected from server +)); + +registerDispatcher(EventType.RECONNECT_SCHEDULED,data => ( + + <>{format_time(data.timeout, tr("now"))} + +)); + +registerDispatcher(EventType.RECONNECT_CANCELED,() => ( + Reconnect canceled. +)); + +registerDispatcher(EventType.RECONNECT_CANCELED,() => ( + Reconnecting... +)); + + +registerDispatcher(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; + + if(data.invoker.client_id > 0) + return ( +
+ + + {time} + {reason} + +
+ ); + else + + return ( +
+ + {time} + {reason} + +
+ ); +}); + +registerDispatcher(EventType.SERVER_REQUIRES_PASSWORD,() => ( + Server requires a password to connect. +)); + +registerDispatcher(EventType.SERVER_CLOSED,data => { + if(data.message) + return ( + + <>{data.message} + + ); + return Server has been closed.; +}); + +registerDispatcher(EventType.CONNECTION_COMMAND_ERROR,data => { + let message; + if(typeof data.error === "string") + message = data.error; + else if(data.error instanceof CommandResult) + message = data.error.formattedMessage(); + else + message = data.error + ""; + + return ( +
+ + <>{message} + +
+ ) +}); + + +registerDispatcher(EventType.CHANNEL_CREATE,(data, handlerId) => ( + + + + +)); + +registerDispatcher(EventType.CHANNEL_CREATE_OWN,(data, handlerId) => ( + + + +)); + +registerDispatcher(EventType.CHANNEL_DELETE,(data, handlerId) => ( + + + + +)); + +registerDispatcher(EventType.CHANNEL_DELETE_OWN,(data, handlerId) => ( + + + +)); + + +registerDispatcher(EventType.CLIENT_POKE_SEND,(data, handlerId) => ( + + + +)); + +registerDispatcher(EventType.CLIENT_POKE_RECEIVED,(data, handlerId) => { + if(data.message) { + return ( + + + + + ); + } else { + return ( + + + + ); + } +}); + +registerDispatcher(EventType.PRIVATE_MESSAGE_RECEIVED, () => undefined); +registerDispatcher(EventType.PRIVATE_MESSAGE_SEND, () => undefined); \ No newline at end of file diff --git a/shared/js/ui/frames/log/DispatcherNotifications.ts b/shared/js/ui/frames/log/DispatcherNotifications.ts new file mode 100644 index 00000000..9323dee3 --- /dev/null +++ b/shared/js/ui/frames/log/DispatcherNotifications.ts @@ -0,0 +1,480 @@ +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 "tc-shared/ui/frames/log/Definitions"; +import {server_connections} from "tc-shared/ui/frames/connection_handlers"; +import {renderBBCodeAsText} from "tc-shared/text/bbcode"; +import {format_time} from "tc-shared/ui/frames/chat"; +import {ViewReasonId} from "tc-shared/ConnectionHandler"; +import {findLogDispatcher} from "tc-shared/ui/frames/log/DispatcherLog"; +import {formatDate} from "tc-shared/MessageFormatter"; +import {Settings, settings} from "tc-shared/settings"; + +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; + +let windowFocused = false; + +document.addEventListener("focusin", () => windowFocused = true); +document.addEventListener("focusout", () => windowFocused = false); + +const dispatchers: {[key: string]: DispatcherLog} = { }; +function registerDispatcher(key: T, builder: DispatcherLog) { + dispatchers[key] = builder; +} + +export function findNotificationDispatcher(type: T) : DispatcherLog { + if(!isNotificationEnabled(type as any)) + return undefined; + return dispatchers[type]; +} + +export function getRegisteredNotificationDispatchers() : TypeInfo[] { + return Object.keys(dispatchers) as any; +} + +export function isNotificationEnabled(type: EventType) { + return settings.global(Settings.FN_EVENTS_NOTIFICATION_ENABLED(type), notificationDefaultStatus[type as any] || false); +} + +const kDefaultIcon = "img/teaspeak_cup_animated.png"; + +async function resolveAvatarUrl(client: EventClient, handlerId: string) { + const connection = server_connections.findConnection(handlerId); + const avatar = connection.fileManager.avatars.resolveClientAvatar({ clientUniqueId: client.client_unique_id, id: client.client_id }); + await avatar.awaitLoaded(); + return avatar.getAvatarUrl(); +} + +async function resolveServerIconUrl(handlerId: string) { + const connection = server_connections.findConnection(handlerId); + + if(connection.channelTree.server.properties.virtualserver_icon_id) { + const icon = connection.fileManager.icons.load_icon(connection.channelTree.server.properties.virtualserver_icon_id); + await icon.await_loading(); + return icon.loaded_url; + } + + return kDefaultIcon; +} + +function spawnNotification(title: string, options: NotificationOptions) { + if(!options.icon) + options.icon = kDefaultIcon; + + if('Notification' in window) { + try { + new Notification(title, options); + } catch (error) { + console.error(error); + } + } +} + +function spawnServerNotification(handlerId: string, options: NotificationOptions) { + resolveServerIconUrl(handlerId).then(iconUrl => { + const connection = server_connections.findConnection(handlerId); + if(!connection) return; + + options.icon = iconUrl; + spawnNotification(connection.channelTree.server.properties.virtualserver_name, options); + }); +} + +function spawnClientNotification(handlerId: string, client: EventClient, options: NotificationOptions) { + resolveAvatarUrl(client, handlerId).then(avatarUrl => { + const connection = server_connections.findConnection(handlerId); + if(!connection) return; + + options.icon = avatarUrl; + spawnNotification(connection.channelTree.server.properties.virtualserver_name, options); + }); +} + +const formatServerAddress = (address: EventServerAddress) => address.server_hostname + (address.server_port === 9987 ? "" : ":" + address.server_port); + +registerDispatcher(EventType.CONNECTION_BEGIN, data => { + spawnNotification(tr("Connecting..."), { + body: tra("Connecting to {}", formatServerAddress(data.address)) + }); +}); + +/* Snipped CONNECTION_HOSTNAME_RESOLVE */ + +registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVED, data => { + spawnNotification(tr("Hostname resolved"), { + body: tra("Hostname resolved successfully to {}", formatServerAddress(data.address)) + }); +}); + +registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVE_ERROR, data => { + spawnNotification(tr("Connect failed"), { + body: tra("Failed to resolve hostname.\nConnecting to given hostname.\nError: {0}", data.message) + }); +}); + +/* Snipped CONNECTION_LOGIN */ + +registerDispatcher(EventType.CONNECTION_CONNECTED, data => { + spawnNotification(tra("Connected to {}", formatServerAddress(data.serverAddress)), { + body: tra("You connected as {}", data.own_client.client_name) + }); +}); + +registerDispatcher(EventType.CONNECTION_FAILED, data => { + spawnNotification(tra("Connection to {} failed", formatServerAddress(data.serverAddress)), { + body: tra("Failed to connect to {}.", formatServerAddress(data.serverAddress)) + }); +}); + +registerDispatcher(EventType.DISCONNECTED, () => { + spawnNotification(tra("You disconnected from the server"), { }); +}); + + +/* snipped RECONNECT_SCHEDULED */ +/* snipped RECONNECT_EXECUTE */ +/* snipped RECONNECT_CANCELED */ + +registerDispatcher(EventType.CONNECTION_VOICE_SETUP_FAILED, (data, handlerId) => { + spawnServerNotification(handlerId, { + body: tra("Failed to setup voice bridge: {0}. Allow reconnect: {1}", data.reason, data.reconnect_delay > 0 ? tr("Yes") : tr("No")) + }); +}); + +registerDispatcher(EventType.CONNECTION_COMMAND_ERROR, (data, handlerId) => { + spawnServerNotification(handlerId, { + body: tra("Command execution resulted in an error.") + }); +}); + +registerDispatcher(EventType.SERVER_WELCOME_MESSAGE, (data, handlerId) => { + spawnServerNotification(handlerId, { + body: tra("Welcome message:\n{}", data.message) + }); +}); + +registerDispatcher(EventType.SERVER_HOST_MESSAGE, (data, handlerId) => { + spawnServerNotification(handlerId, { + body: tra("Host message:\n{}", data.message) + }); +}); + +registerDispatcher(EventType.SERVER_HOST_MESSAGE_DISCONNECT, (data) => { + spawnNotification(tr("Connection to server denied"), { + body: tra("Server message:\n{}", data.message) + }); +}); + + +registerDispatcher(EventType.SERVER_CLOSED, (data, handlerId) => { + spawnServerNotification(handlerId, { + body: data.message ? tra("Server has been closed ({})", data.message) : tr("Server has been closed") + }); +}); + +registerDispatcher(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 : ""; + + 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) + }); +}); + +registerDispatcher(EventType.SERVER_REQUIRES_PASSWORD, () => { + spawnNotification(tra("Failed to connect to the server"), { + body: tr("Server requires a password to connect.") + }); +}); + + +registerDispatcher(EventType.CLIENT_VIEW_ENTER, (data, handlerId) => { + let message; + + switch (data.reason) { + case ViewReasonId.VREASON_USER_ACTION: + if(data.channel_from) { + message = tra("{0} appeared from {1} to {2}", data.client.client_name, data.channel_from.channel_name, data.channel_to.channel_name); + } else { + message = tra("{0} appeared to channel {1}", data.client.client_name, data.channel_to.channel_name); + } + break; + + case ViewReasonId.VREASON_MOVED: + if(data.channel_from) { + message = tra("{0} appeared from {1} to {2}, moved by {3}", data.client.client_name, data.channel_from.channel_name, data.channel_to.channel_name, data.invoker.client_name); + } else { + message = tra("{0} appeared to {1}, moved by {2}", data.client.client_name, data.channel_to.channel_name, data.invoker.client_name); + } + break; + + case ViewReasonId.VREASON_CHANNEL_KICK: + if(data.channel_from) { + message = tra("{0} appeared from {1} to {2}, kicked by {3}{4}", data.client.client_name, data.channel_from.channel_name, data.channel_to.channel_name, data.invoker.client_name, data.message ? " (" + data.message + ")" : ""); + } else { + message = tra("{0} appeared to {1}, kicked by {2}{3}", data.client.client_name, data.channel_to.channel_name, data.invoker.client_name, data.message ? " (" + data.message + ")" : ""); + } + break; + + default: + return; + } + + spawnClientNotification(handlerId, data.client, { + body: message + }); +}); + +registerDispatcher(EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL, (data, handlerId) => { + let message; + + switch (data.reason) { + case ViewReasonId.VREASON_USER_ACTION: + if(data.channel_from) { + message = tra("{0} appeared from {1} to your channel {2}", data.client.client_name, data.channel_from.channel_name, data.channel_to.channel_name); + } else { + message = tra("{0} appeared to your channel {1}", data.client.client_name, data.channel_to.channel_name); + } + break; + + case ViewReasonId.VREASON_MOVED: + if(data.channel_from) { + message = tra("{0} appeared from {1} to your channel {2}, moved by {3}", data.client.client_name, data.channel_from.channel_name, data.channel_to.channel_name, data.invoker.client_name); + } else { + message = tra("{0} appeared to your channel {1}, moved by {2}", data.client.client_name, data.channel_to.channel_name, data.invoker.client_name); + } + break; + + case ViewReasonId.VREASON_CHANNEL_KICK: + if(data.channel_from) { + message = tra("{0} appeared from {1} to your channel {2}, kicked by {3}{4}", data.client.client_name, data.channel_from.channel_name, data.channel_to.channel_name, data.invoker.client_name, data.message ? " (" + data.message + ")" : ""); + } else { + message = tra("{0} appeared to your channel {1}, kicked by {2}{3}", data.client.client_name, data.channel_to.channel_name, data.invoker.client_name, data.message ? " (" + data.message + ")" : ""); + } + break; + + default: + return; + } + + spawnClientNotification(handlerId, data.client, { + body: message + }); +}); + +registerDispatcher(EventType.CLIENT_VIEW_MOVE, (data, handlerId) => { + let message; + + switch (data.reason) { + case ViewReasonId.VREASON_MOVED: + message = tra("{0} was moved from channel {1} to {2} by {3}", data.client.client_name, data.channel_from.channel_name, data.channel_to.channel_name, data.invoker.client_name); + break; + + case ViewReasonId.VREASON_USER_ACTION: + message = tra("{0} switched from channel {1} to {2}", data.client.client_name, data.channel_from.channel_name, data.channel_to.channel_name); + break; + + case ViewReasonId.VREASON_CHANNEL_KICK: + message = tra("{0} got kicked from channel {1} to {2} by {3}{4}", data.client.client_name, data.channel_from.channel_name, data.channel_to.channel_name, data.invoker.client_name, data.message ? " (" + data.message + ")" : ""); + break; + + default: + return; + } + + spawnClientNotification(handlerId, data.client, { + body: message + }); +}); + +registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL, findLogDispatcher(EventType.CLIENT_VIEW_MOVE)); + +registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN, (data, handlerId) => { + let message; + + switch (data.reason) { + case ViewReasonId.VREASON_MOVED: + message = tra("You have been moved by {3} from channel {1} to {2}", data.client.client_name, data.channel_from.channel_name, data.channel_to.channel_name, data.invoker.client_name); + break; + + case ViewReasonId.VREASON_USER_ACTION: + /* no need to notify here */ + return; + + case ViewReasonId.VREASON_CHANNEL_KICK: + message = tra("You got kicked out of the channel {1} to channel {2} by {3}{4}", data.client.client_name, data.channel_from.channel_name, data.channel_to.channel_name, data.invoker.client_name, data.message ? " (" + data.message + ")" : ""); + break; + + default: + return; + } + + spawnClientNotification(handlerId, data.client, { + body: message + }); +}); + + +registerDispatcher(EventType.CLIENT_VIEW_LEAVE, (data, handlerId) => { + let message; + + switch (data.reason) { + case ViewReasonId.VREASON_USER_ACTION: + message = tra("{0} disappeared from {1} to {2}", data.client.client_name, data.channel_from.channel_name, data.channel_to.channel_name); + break; + + case ViewReasonId.VREASON_SERVER_LEFT: + message = tra("{0} left the server{1}", data.client.client_name, data.message ? " (" + data.message + ")" : ""); + break; + + case ViewReasonId.VREASON_SERVER_KICK: + message = tra("{0} was kicked from the server by {1}.{2}", data.client, data.invoker.client_name, data.message ? " (" + data.message + ")" : ""); + break; + + case ViewReasonId.VREASON_CHANNEL_KICK: + message = tra("{0} was kicked from channel {1} by {2}.{3}", data.client, data.channel_from.channel_name, data.invoker.client_name, data.message ? " (" + data.message + ")" : ""); + break; + + case ViewReasonId.VREASON_BAN: + let duration = "permanently"; + if(data.ban_time) + duration = tr("for") + " " + formatDate(data.ban_time); + + message = tra("{0} was banned {1} by {2}.{3}", data.client.client_name, duration, data.invoker.client_name, data.message ? " (" + data.message + ")" : ""); + break; + + case ViewReasonId.VREASON_TIMEOUT: + message = tra("{0} timed out{1}", data.client.client_name, data.message ? " (" + data.message + ")" : ""); + break; + + case ViewReasonId.VREASON_MOVED: + message = tra("{0} disappeared from {1} to {2}, moved by {3}", data.client.client_name, data.channel_from.channel_name, data.channel_to.channel_name, data.invoker.client_name); + break; + + default: + return; + } + + spawnClientNotification(handlerId, data.client, { + body: message + }); +}); + +registerDispatcher(EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL, (data, handlerId) => { + let message; + + switch (data.reason) { + case ViewReasonId.VREASON_USER_ACTION: + message = tra("{0} disappeared from your channel {1} to {2}", data.client.client_name, data.channel_from.channel_name, data.channel_to.channel_name); + break; + + case ViewReasonId.VREASON_MOVED: + message = tra("{0} disappeared from your channel {1} to {2}, moved by {3}", data.client.client_name, data.channel_from.channel_name, data.channel_to.channel_name, data.invoker.client_name); + break; + + default: + return findLogDispatcher(EventType.CLIENT_VIEW_LEAVE)(data, handlerId, EventType.CLIENT_VIEW_LEAVE); + } + + spawnClientNotification(handlerId, data.client, { + body: message + }); +}); + + +registerDispatcher(EventType.CLIENT_NICKNAME_CHANGED, (data, handlerId) => { + spawnClientNotification(handlerId, data.client, { + body: tra("{0} changed his nickname from \"{1}\" to \"{2}\"", data.client.client_name, data.old_name, data.new_name) + }); +}); + +/* snipped CLIENT_NICKNAME_CHANGED_OWN */ +/* snipped CLIENT_NICKNAME_CHANGE_FAILED */ + +registerDispatcher(EventType.CHANNEL_CREATE, (data, handlerId) => { + spawnServerNotification(handlerId, { + body: tra("Channel {} has been created by {}.", data.channel.channel_name, data.creator.client_name) + }); +}); + +registerDispatcher(EventType.CHANNEL_DELETE, (data, handlerId) => { + spawnServerNotification(handlerId, { + body: tra("Channel {} has been deleted by {}.", data.channel.channel_name, data.deleter.client_name) + }); +}); + +/* snipped CHANNEL_CREATE_OWN */ +/* snipped CHANNEL_DELETE_OWN */ + +/* snipped ERROR_CUSTOM */ +/* snipped ERROR_PERMISSION */ + +/* TODO! +CLIENT_SERVER_GROUP_ADD = "client.server.group.add", +CLIENT_SERVER_GROUP_REMOVE = "client.server.group.remove", +CLIENT_CHANNEL_GROUP_CHANGE = "client.channel.group.change", +*/ + +registerDispatcher(EventType.CLIENT_POKE_RECEIVED, (data, handlerId) => { + resolveAvatarUrl(data.sender, handlerId).then(avatarUrl => { + const connection = server_connections.findConnection(handlerId); + if(!connection) return; + + new Notification(connection.channelTree.server.properties.virtualserver_name, { + body: tr("You've peen poked by") + " " + data.sender.client_name + (data.message ? ":\n" + renderBBCodeAsText(data.message) : ""), + icon: avatarUrl + }); + }); +}); + +/* snipped CLIENT_POKE_SEND */ + +registerDispatcher(EventType.GLOBAL_MESSAGE, (data, handlerId) => { + if(windowFocused) + return; + + spawnServerNotification(handlerId, { + body: tra("{} send a server message: {}", data.sender.client_name, renderBBCodeAsText(data.message)), + }); +}); + + +registerDispatcher(EventType.PRIVATE_MESSAGE_RECEIVED, (data, handlerId) => { + if(windowFocused) + return; + + spawnClientNotification(handlerId, data.sender, { + body: tra("Private message from {}: {}", data.sender.client_name, renderBBCodeAsText(data.message)), + }); +}); + +/* snipped PRIVATE_MESSAGE_SEND */ + +loader.register_task(Stage.LOADED, { + function: async () => { + if(!('Notification' in window)) + return; + + if(Notification.permission === "granted") + return; + + Notification.requestPermission().then(result => { + log.info(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); + }); + }, + name: "Request notifications", + priority: 1 +}); \ No newline at end of file diff --git a/shared/js/ui/frames/log/Renderer.scss b/shared/js/ui/frames/log/Renderer.scss new file mode 100644 index 00000000..0140a200 --- /dev/null +++ b/shared/js/ui/frames/log/Renderer.scss @@ -0,0 +1,55 @@ +@import "../../../../css/static/mixin"; + +.htmlTag { + display: flex; + flex-direction: column; + justify-content: stretch; + + flex-shrink: 1; + min-height: 2em; +} + +.logContainer { + flex-shrink: 1; + flex-grow: 1; + + display: flex; + flex-direction: column; + justify-content: flex-start; + + min-height: 2em; + + overflow-x: hidden; + overflow-y: auto; + + @include chat-scrollbar-vertical(); +} + +.logEntry { + flex-shrink: 0; + flex-grow: 0; + + color: var(--server-log-text); + + overflow-x: hidden; + overflow-y: hidden; + + display: block; + + > *, .errorMessage > * { + display: inline-block; + + overflow-wrap: break-word; + word-wrap: break-word; + + max-width: 100%; + } + + .timestamp { + padding-right: 5px; + } + + .errorMessage { + color: var(--server-log-error); + } +} \ No newline at end of file diff --git a/shared/js/ui/frames/log/Renderer.tsx b/shared/js/ui/frames/log/Renderer.tsx new file mode 100644 index 00000000..60abc552 --- /dev/null +++ b/shared/js/ui/frames/log/Renderer.tsx @@ -0,0 +1,89 @@ +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 * as React from "react"; +import {findLogDispatcher} from "tc-shared/ui/frames/log/DispatcherLog"; + +const cssStyle = require("./Renderer.scss"); + +const LogFallbackDispatcher = (_unused, __unused, eventType) => ( +
+ + <>{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); + + if(!rendered) /* hide message */ + return null; + + const date = new Date(props.entry.timestamp); + return ( +
+
<{ + ("0" + date.getHours()).substr(-2) + ":" + + ("0" + date.getMinutes()).substr(-2) + ":" + + ("0" + date.getSeconds()).substr(-2) + }>
+ {rendered} +
+ ); +}); + +export const ServerLogRenderer = (props: { events: Registry, handlerId: string }) => { + const [ logs, setLogs ] = useState(() => { + props.events.fire_async("query_log"); + return "loading"; + }); + + const [ revision, setRevision ] = useState(0); + + const refContainer = useRef(); + const scrollOffset = useRef("bottom"); + + props.events.reactUse("notify_log", event => { + const logs = event.log.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 => { + if(logs === "loading") + return; + + logs.push(event.event); + logs.splice(0, Math.max(0, logs.length - 100)); + logs.sort((a, b) => a.timestamp - b.timestamp); + setRevision(revision + 1); + }); + + useEffect(() => { + const id = requestAnimationFrame(() => { + if(!refContainer.current) + return; + + refContainer.current.scrollTop = scrollOffset.current === "bottom" ? refContainer.current.scrollHeight : scrollOffset.current; + }); + return () => cancelAnimationFrame(id); + }); + + return ( +
{ + const target = event.target as HTMLDivElement; + + const top = target.scrollTop; + const total = target.scrollHeight - target.clientHeight; + const shouldFollow = top + 50 > total; + + scrollOffset.current = shouldFollow ? "bottom" : top; + }}> + {logs === "loading" ? null : logs.map(e => )} +
+ ); +}; diff --git a/shared/js/ui/frames/log/ServerEventLog.tsx b/shared/js/ui/frames/log/ServerEventLog.tsx new file mode 100644 index 00000000..8c9a346f --- /dev/null +++ b/shared/js/ui/frames/log/ServerEventLog.tsx @@ -0,0 +1,65 @@ +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} from "tc-shared/ui/frames/log/DispatcherNotifications"; + +const cssStyle = require("./Renderer.scss"); + +let uniqueLogEventId = 0; +export class ServerEventLog { + private readonly connection: ConnectionHandler; + private readonly uiEvents: Registry; + 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_async("notify_log", { log: this.eventLog }); + }); + + ReactDOM.render(, this.htmlTag); + } + + log(type: T, data: TypeInfo[T]) { + const event = { + data: data, + timestamp: Date.now(), + type: type as any, + uniqueId: "log-" + Date.now() + "-" + (++uniqueLogEventId) + }; + + this.eventLog.push(event); + while(this.eventLog.length > this.maxHistoryLength) + this.eventLog.pop_front(); + + this.uiEvents.fire_async("notify_log_add", { event: event }); + + const notification = findNotificationDispatcher(type); + if(notification) notification(data, this.connection.handlerId, type); + } + + getHTMLTag() { + return this.htmlTag; + } + + destroy() { + if(this.htmlTag) { + ReactDOM.unmountComponentAtNode(this.htmlTag); + this.htmlTag?.remove(); + this.htmlTag = undefined; + } + + this.eventLog = undefined; + } +} \ No newline at end of file diff --git a/shared/js/ui/frames/server_log.ts b/shared/js/ui/frames/server_log.ts deleted file mode 100644 index 80c91ab8..00000000 --- a/shared/js/ui/frames/server_log.ts +++ /dev/null @@ -1,611 +0,0 @@ -import {tra, traj} from "tc-shared/i18n/localize"; -import {PermissionInfo} from "tc-shared/permission/PermissionManager"; -import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler"; -import * as htmltags from "tc-shared/ui/htmltags"; -import {format_time, formatMessage} from "tc-shared/ui/frames/chat"; -import {formatDate} from "tc-shared/MessageFormatter"; -import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; -import {BBCodeRenderOptions, renderBBCodeAsJQuery} from "tc-shared/text/bbcode"; - -export enum Type { - 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_SETUP_FAILED = "connection_voice_setup_failed", - 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_NICKNAME_CHANGED = "client_nickname_changed", - 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", - - CHANNEL_CREATE = "channel_create", - CHANNEL_DELETE = "channel_delete", - - ERROR_CUSTOM = "error_custom", - ERROR_PERMISSION = "error_permission", - - RECONNECT_SCHEDULED = "reconnect_scheduled", - RECONNECT_EXECUTE = "reconnect_execute", - RECONNECT_CANCELED = "reconnect_canceled" -} - -export namespace base { - export type Client = { - client_unique_id: string; - client_name: string; - client_id: number; - } - export type Channel = { - channel_id: number; - channel_name: string; - } - export type Server = { - server_name: string; - server_unique_id: string; - } - export type ServerAddress = { - server_hostname: string; - server_port: number; - } -} - -export namespace event { - export type GlobalMessage = { - sender: base.Client; - message: string; - } - export type ConnectBegin = { - address: base.ServerAddress; - client_nickname: string; - } - export type ErrorCustom = { - message: string; - } - - export type ReconnectScheduled = { - timeout: number; - } - - export type ReconnectCanceled = { } - export type ReconnectExecute = { } - - export type ErrorPermission = { - permission: PermissionInfo; - } - - export type WelcomeMessage = { - message: string; - } - - export type HostMessageDisconnect = { - message: string; - } - - export type ClientMove = { - channel_from?: base.Channel; - channel_from_own: boolean; - - channel_to?: base.Channel; - channel_to_own: boolean; - - client: base.Client; - client_own: boolean; - - invoker?: base.Client; - - message?: string; - reason: ViewReasonId; - } - - export type ClientEnter = { - channel_from?: base.Channel; - channel_to?: base.Channel; - - client: base.Client; - invoker?: base.Client; - - message?: string; - own_channel: boolean; - - reason: ViewReasonId; - ban_time?: number; - } - - export type ClientLeave = { - channel_from?: base.Channel; - channel_to?: base.Channel; - - client: base.Client; - invoker?: base.Client; - - message?: string; - own_channel: boolean; - - reason: ViewReasonId; - ban_time?: number; - } - - export type ChannelCreate = { - creator: base.Client; - channel: base.Channel; - - own_action: boolean; - } - - export type ChannelDelete = { - deleter: base.Client; - channel: base.Channel; - - own_action: boolean; - } - - export type ConnectionConnected = { - own_client: base.Client; - } - export type ConnectionFailed = {}; - export type ConnectionLogin = {} - export type ConnectionHostnameResolve = {}; - export type ConnectionHostnameResolved = { - address: base.ServerAddress; - } - export type ConnectionHostnameResolveError = { - message: string; - } - - export type ConnectionVoiceSetupFailed = { - reason: string; - reconnect_delay: number; /* if less or equal to 0 reconnect is prohibited */ - } - - export type ConnectionCommandError = { - error: any; - } - - export type ClientNicknameChanged = { - own_client: boolean; - - client: base.Client; - - old_name: string; - new_name: string; - } - - export type ClientNicknameChangeFailed = { - reason: string; - } - - export type ServerClosed = { - message: string; - } - - export type ServerRequiresPassword = {} - - export type ServerBanned = { - message: string; - time: number; - - invoker: base.Client; - } -} - -export type LogMessage = { - type: Type; - timestamp: number; - data: any; -} - -export interface TypeInfo { - "connection_begin" : event.ConnectBegin; - "global_message": event.GlobalMessage; - - "error_custom": event.ErrorCustom; - "error_permission": event.ErrorPermission; - - "connection_hostname_resolved": event.ConnectionHostnameResolved; - "connection_hostname_resolve": event.ConnectionHostnameResolve; - "connection_hostname_resolve_error": event.ConnectionHostnameResolveError; - "connection_failed": event.ConnectionFailed; - "connection_login": event.ConnectionLogin; - "connection_connected": event.ConnectionConnected; - "connection_voice_setup_failed": event.ConnectionVoiceSetupFailed; - "connection_command_error": event.ConnectionCommandError; - - "reconnect_scheduled": event.ReconnectScheduled; - "reconnect_canceled": event.ReconnectCanceled; - "reconnect_execute": event.ReconnectExecute; - - "server_welcome_message": event.WelcomeMessage; - "server_host_message": event.WelcomeMessage; - "server_host_message_disconnect": event.HostMessageDisconnect; - - "server_closed": event.ServerClosed; - "server_requires_password": event.ServerRequiresPassword; - "server_banned": event.ServerBanned; - - "client_view_enter": event.ClientEnter; - "client_view_move": event.ClientMove; - "client_view_leave": event.ClientLeave; - - "client_nickname_change_failed": event.ClientNicknameChangeFailed, - "client_nickname_changed": event.ClientNicknameChanged, - - "channel_create": event.ChannelCreate; - "channel_delete": event.ChannelDelete; - - "disconnected": any; -} - -export type MessageBuilderOptions = {}; -export type MessageBuilder = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | string | undefined; - -export const MessageBuilders: {[key: string]: MessageBuilder} = { - "error_custom": (data: event.ErrorCustom) => { - return [$.spawn("div").addClass("log-error").text(data.message)] - } -}; -function register_message_builder(key: T, builder: MessageBuilder) { - MessageBuilders[key] = builder; -} - -export class ServerLog { - private readonly handle: ConnectionHandler; - private history_length: number = 100; - - private _log: LogMessage[] = []; - private _html_tag: JQuery; - private _log_container: JQuery; - private auto_follow: boolean; /* automatic scroll to bottom */ - private _ignore_event: number; /* after auto scroll we've triggered the scroll event. We want to prevent this so we capture the next event */ - - constructor(handle: ConnectionHandler) { - this.handle = handle; - this.auto_follow = true; - - this._html_tag = $.spawn("div").addClass("container-log"); - this._log_container = $.spawn("div").addClass("container-messages"); - this._log_container.appendTo(this._html_tag); - - this._html_tag.on('scroll', event => { - if(Date.now() - this._ignore_event < 100) { - this._ignore_event = 0; - return; - } - - this.auto_follow = (this._html_tag[0].scrollTop + this._html_tag[0].clientHeight + this._html_tag[0].clientHeight * .125) > this._html_tag[0].scrollHeight; - }); - } - - log(type: T, data: TypeInfo[T]) { - const event = { - data: data, - timestamp: Date.now(), - type: type as any - }; - - this._log.push(event); - while(this._log.length > this.history_length) - this._log.pop_front(); - - this.append_log(event); - } - - html_tag() : JQuery { - return this._html_tag; - } - - destroy() { - this._html_tag && this._html_tag.remove(); - this._html_tag = undefined; - this._log_container = undefined; - - this._log = undefined; - } - - private _scroll_task: number; - - private append_log(message: LogMessage) { - let container = $.spawn("div").addClass("log-message"); - - /* build timestamp */ - { - const num = number => ('00' + number).substr(-2); - const date = new Date(message.timestamp); - $.spawn("div") - .addClass("timestamp") - .text("<" + num(date.getHours()) + ":" + num(date.getMinutes()) + ":" + num(date.getSeconds()) + ">") - .appendTo(container); - } - - /* build message data */ - { - const builder = MessageBuilders[message.type]; - if(!builder) { - formatMessage(tr("missing log message builder {0}!"), message.type).forEach(e => e.addClass("log-error").appendTo(container)); - } else { - const elements = builder(message.data, {}); - if(!elements || elements.length == 0) - return; /* discard message */ - container.append(...elements); - } - } - this._ignore_event = Date.now(); - this._log_container.append(container); - - /* max history messages! */ - const messages = this._log_container.children(); - let index = 0; - while(messages.length - index > this.history_length) - index++; - const hide_elements = messages.filter(idx => idx < index); - hide_elements.hide(250, () => hide_elements.remove()); - - if(this.auto_follow) { - clearTimeout(this._scroll_task); - - /* do not enforce a recalculate style here */ - this._scroll_task = setTimeout(() => { - this._html_tag.scrollTop(this._html_tag[0].scrollHeight); - this._scroll_task = 0; - }, 5) as any; - } - } -} - -/* impl of the parsers */ -const client_tag = (client: base.Client, braces?: boolean) => htmltags.generate_client_object({ - client_unique_id: client.client_unique_id, - client_id: client.client_id, - client_name: client.client_name, - add_braces: braces -}); -const channel_tag = (channel: base.Channel, braces?: boolean) => htmltags.generate_channel_object({ - channel_display_name: channel.channel_name, - channel_name: channel.channel_name, - channel_id: channel.channel_id, - add_braces: braces -}); - -MessageBuilders["connection_begin"] = (data: event.ConnectBegin) => { - return formatMessage(tr("Connecting to {0}{1}"), data.address.server_hostname, data.address.server_port == 9987 ? "" : (":" + data.address.server_port)); -}; - -MessageBuilders["connection_hostname_resolve"] = (data: event.ConnectionHostnameResolve) => formatMessage(tr("Resolving hostname")); -MessageBuilders["connection_hostname_resolved"] = (data: event.ConnectionHostnameResolved) => formatMessage(tr("Hostname resolved successfully to {0}:{1}"), data.address.server_hostname, data.address.server_port); -MessageBuilders["connection_hostname_resolve_error"] = (data: event.ConnectionHostnameResolveError) => formatMessage(tr("Failed to resolve hostname. Connecting to given hostname. Error: {0}"), data.message); - -MessageBuilders["connection_login"] = () => formatMessage(tr("Logging in...")); -MessageBuilders["connection_failed"] = () => formatMessage(tr("Connect failed.")); -MessageBuilders["connection_connected"] = (data: event.ConnectionConnected) => formatMessage(tr("Connected as {0}"), client_tag(data.own_client, true)); - -MessageBuilders["connection_voice_setup_failed"] = (data: event.ConnectionVoiceSetupFailed) => { - return formatMessage(tr("Failed to setup voice bridge: {0}. Allow reconnect: {1}"), data.reason, data.reconnect_delay > 0 ? tr("yes") : tr("no")); -}; - -MessageBuilders["error_permission"] = (data: event.ErrorPermission) => { - return formatMessage(tr("Insufficient client permissions. Failed on permission {0}"), data.permission ? data.permission.name : "unknown").map(e => e.addClass("log-error")); -}; - -MessageBuilders["client_view_enter"] = (data: event.ClientEnter) => { - if(data.reason == ViewReasonId.VREASON_SYSTEM) { - return undefined; - } if(data.reason == ViewReasonId.VREASON_USER_ACTION) { - /* client appeared */ - if(data.channel_from) { - return formatMessage(data.own_channel ? tr("{0} appeared from {1} to your {2}") : tr("{0} appeared from {1} to {2}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to)); - } else { - return formatMessage(data.own_channel ? tr("{0} connected to your channel {1}") : tr("{0} connected to channel {1}"), client_tag(data.client), channel_tag(data.channel_to)); - } - } else if(data.reason == ViewReasonId.VREASON_MOVED) { - if(data.channel_from) { - return formatMessage(data.own_channel ? tr("{0} appeared from {1} to your channel {2}, moved by {3}") : tr("{0} appeared from {1} to {2}, moved by {3}"), - client_tag(data.client), - channel_tag(data.channel_from), - channel_tag(data.channel_to), - client_tag(data.invoker) - ); - } else { - return formatMessage(data.own_channel ? tr("{0} appeared to your channel {1}, moved by {2}") : tr("{0} appeared to {1}, moved by {2}"), - client_tag(data.client), - channel_tag(data.channel_to), - client_tag(data.invoker) - ); - } - } else if(data.reason == ViewReasonId.VREASON_CHANNEL_KICK) { - if(data.channel_from) { - return formatMessage(data.own_channel ? tr("{0} appeared from {1} to your channel {2}, kicked by {3}{4}") : tr("{0} appeared from {1} to {2}, kicked by {3}{4}"), - client_tag(data.client), - channel_tag(data.channel_from), - channel_tag(data.channel_to), - client_tag(data.invoker), - data.message ? (" (" + data.message + ")") : "" - ); - } else { - return formatMessage(data.own_channel ? tr("{0} appeared to your channel {1}, kicked by {2}{3}") : tr("{0} appeared to {1}, kicked by {2}{3}"), - client_tag(data.client), - channel_tag(data.channel_to), - client_tag(data.invoker), - data.message ? (" (" + data.message + ")") : "" - ); - } - } - return [$.spawn("div").addClass("log-error").text("Invalid view enter reason id (" + data.message + ")")]; -}; - -MessageBuilders["client_view_move"] = (data: event.ClientMove) => { - if(data.reason == ViewReasonId.VREASON_MOVED) { - return formatMessage(data.client_own ? tr("You was moved by {3} from channel {1} to {2}") : tr("{0} was moved from channel {1} to {2} by {3}"), - client_tag(data.client), - channel_tag(data.channel_from), - channel_tag(data.channel_to), - client_tag(data.invoker) - ); - } else if(data.reason == ViewReasonId.VREASON_USER_ACTION) { - return formatMessage(data.client_own ? tr("You switched from channel {1} to {2}") : tr("{0} switched from channel {1} to {2}"), - client_tag(data.client), - channel_tag(data.channel_from), - channel_tag(data.channel_to) - ); - } else if(data.reason == ViewReasonId.VREASON_CHANNEL_KICK) { - return formatMessage(data.client_own ? tr("You got kicked out of the channel {1} to channel {2} by {3}{4}") : tr("{0} got kicked from channel {1} to {2} by {3}{4}"), - client_tag(data.client), - channel_tag(data.channel_from), - channel_tag(data.channel_to), - client_tag(data.invoker), - data.message ? (" (" + data.message + ")") : "" - ); - } - return [$.spawn("div").addClass("log-error").text("Invalid view move reason id (" + data.reason + ")")]; -}; - -MessageBuilders["client_view_leave"] = (data: event.ClientLeave) => { - if(data.reason == ViewReasonId.VREASON_USER_ACTION) { - return formatMessage(data.own_channel ? tr("{0} disappeared from your channel {1} to {2}") : tr("{0} disappeared from {1} to {2}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to)); - } else if(data.reason == ViewReasonId.VREASON_SERVER_LEFT) { - return formatMessage(tr("{0} left the server{1}"), client_tag(data.client), data.message ? (" (" + data.message + ")") : ""); - } else if(data.reason == ViewReasonId.VREASON_SERVER_KICK) { - return formatMessage(tr("{0} was kicked from the server by {1}.{2}"), client_tag(data.client), client_tag(data.invoker), data.message ? (" (" + data.message + ")") : ""); - } else if(data.reason == ViewReasonId.VREASON_CHANNEL_KICK) { - return formatMessage(data.own_channel ? tr("{0} was kicked from your channel by {2}.{3}") : tr("{0} was kicked from channel {1} by {2}.{3}"), - client_tag(data.client), - channel_tag(data.channel_from), - client_tag(data.invoker), - data.message ? (" (" + data.message + ")") : "" - ); - } else if(data.reason == ViewReasonId.VREASON_BAN) { - let duration = "permanently"; - if(data.ban_time) - duration = "for " + formatDate(data.ban_time); - return formatMessage(tr("{0} was banned {1} by {2}.{3}"), - client_tag(data.client), - duration, - client_tag(data.invoker), - data.message ? (" (" + data.message + ")") : "" - ); - } else if(data.reason == ViewReasonId.VREASON_TIMEOUT) { - return formatMessage(tr("{0} timed out{1}"), client_tag(data.client), data.message ? (" (" + data.message + ")") : ""); - } else if(data.reason == ViewReasonId.VREASON_MOVED) { - return formatMessage(data.own_channel ? tr("{0} disappeared from your channel {1} to {2}, moved by {3}") : tr("{0} disappeared from {1} to {2}, moved by {3}"), client_tag(data.client), channel_tag(data.channel_from), channel_tag(data.channel_to), client_tag(data.invoker)); - } - - return [$.spawn("div").addClass("log-error").text("Invalid view leave reason id (" + data.reason + ")")]; -}; - -const bbcodeRenderOptions: BBCodeRenderOptions = { - convertSingleUrls: false -}; - -MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage) => { - return renderBBCodeAsJQuery("[color=green]" + data.message + "[/color]", bbcodeRenderOptions); -}; - -MessageBuilders["server_host_message"] = (data: event.WelcomeMessage) => { - return renderBBCodeAsJQuery("[color=green]" + data.message + "[/color]", bbcodeRenderOptions); -}; - -MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged) => { - if(data.own_client) { - return tra("Nickname successfully changed."); - } else { - return tra("{0} changed his nickname from \"{1}\" to \"{2}\"", client_tag(data.client), data.old_name, data.new_name); - } -}; - -register_message_builder("client_nickname_change_failed", (data) => { - return tra("Failed to change own client name: {}", data.reason); -}); - -MessageBuilders["global_message"] = () => { - return []; /* we do not show global messages within log */ -}; - -MessageBuilders["disconnected"] = () => formatMessage(tr("Disconnected from server")); - -MessageBuilders["reconnect_scheduled"] = (data: event.ReconnectScheduled) => { - return tra("Reconnecting in {0}.", format_time(data.timeout, tr("now"))) -}; - -MessageBuilders["reconnect_canceled"] = () => { - return tra("Canceled reconnect.") -}; - -MessageBuilders["reconnect_execute"] = () => { - return tra("Reconnecting...") -}; - -MessageBuilders["server_banned"] = (data: event.ServerBanned) => { - let result: JQuery[]; - - const time = data.time == 0 ? tr("ever") : format_time(data.time * 1000, tr("one second")); - if(data.invoker.client_id > 0) { - if(data.message) - result = traj("You've been banned from the server by {0} for {1}. Reason: {2}", client_tag(data.invoker), time, data.message); - else - result = traj("You've been banned from the server by {0} for {1}.", client_tag(data.invoker), time); - } else { - if(data.message) - result = traj("You've been banned from the server for {0}. Reason: {1}", time, data.message); - else - result = traj("You've been banned from the server for {0}.", time); - } - - return result.map(e => e.addClass("log-error")); -}; - -register_message_builder("server_host_message_disconnect", (data) => { - return tra(data.message); -}); - -register_message_builder("server_requires_password", () => { - return tra("Server requires a password to connect."); -}); - -register_message_builder("server_closed", (data) => { - return data.message ? tra("Server has been closed ({}).", data.message) : tra("Server has been closed."); -}); - -register_message_builder("connection_command_error", (data) => { - let error_message; - if(typeof data.error === "string") - error_message = data.error; - else if(data.error instanceof CommandResult) - error_message = data.error.extra_message || data.error.message; - else - error_message = data.error + ""; - return tra("Command execution resulted in: {}", error_message); -}); - -register_message_builder("channel_create", (data) => { - if(data.own_action) - return tra("Channel {} has been created.", channel_tag(data.channel)); - return tra("Channel {} has been created by {}.", channel_tag(data.channel), client_tag(data.creator)); -}); - -register_message_builder("channel_delete", (data) => { - if(data.own_action) - return tra("Channel {} has been deleted.", channel_tag(data.channel)); - return tra("Channel {] has been deleted by {}.", channel_tag(data.channel), client_tag(data.deleter)); -}); \ No newline at end of file diff --git a/shared/js/ui/react-elements/i18n/index.tsx b/shared/js/ui/react-elements/i18n/index.tsx index 974d091e..4e7f620a 100644 --- a/shared/js/ui/react-elements/i18n/index.tsx +++ b/shared/js/ui/react-elements/i18n/index.tsx @@ -1,4 +1,6 @@ import * as React from "react"; +import {parseMessageWithArguments} from "tc-shared/ui/frames/chat"; +import {cloneElement} from "react"; let instances = []; export class Translatable extends React.Component<{ message: string, children?: never } | { children: string }, { translated: string }> { @@ -29,6 +31,29 @@ export class Translatable extends React.Component<{ message: string, children?: } } +export const VariadicTranslatable = (props: { text: string, children?: React.ReactElement[] | React.ReactElement }) => { + const args = Array.isArray(props.children) ? props.children : [props.children]; + const argsUseCount = [...new Array(args.length)].map(() => 0); + + const translated = /* @tr-ignore */ tr(props.text); + + return (<> + { + parseMessageWithArguments(translated, args.length).map(e => { + if(typeof e === "string") + return e; + + let element = args[e]; + if(argsUseCount[e]) + element = cloneElement(element); + argsUseCount[e]++; + + return {element}; + }) + } + ); +}; + declare global { interface Window { diff --git a/shared/js/ui/view.tsx b/shared/js/ui/view.tsx index 46fd5723..b1cc688e 100644 --- a/shared/js/ui/view.tsx +++ b/shared/js/ui/view.tsx @@ -7,7 +7,6 @@ import {PermissionType} from "tc-shared/permission/PermissionType"; import {KeyCode, SpecialKey} from "tc-shared/PPTListener"; import {Sound} from "tc-shared/sound/Sounds"; import {Group} from "tc-shared/permission/GroupManager"; -import * as server_log from "tc-shared/ui/frames/server_log"; import {ServerAddress, ServerEntry} from "tc-shared/ui/server"; import {ChannelEntry, ChannelProperties, ChannelSubscribeMode} from "tc-shared/ui/channel"; import {ClientEntry, LocalClientEntry, MusicClientEntry} from "tc-shared/ui/client"; @@ -27,6 +26,7 @@ import {formatMessage} from "tc-shared/ui/frames/chat"; import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; import {tra} from "tc-shared/i18n/localize"; import {TreeEntryMove} from "tc-shared/ui/tree/TreeEntryMove"; +import {EventType} from "tc-shared/ui/frames/log/Definitions"; export interface ChannelTreeEvents { action_select_entries: { @@ -898,10 +898,9 @@ export class ChannelTree { return new Promise(resolve => { resolve(channel); }) }).then(channel => { - this.client.log.log(server_log.Type.CHANNEL_CREATE, { + this.client.log.log(EventType.CHANNEL_CREATE_OWN, { channel: channel.log_data(), creator: this.client.getClient().log_data(), - own_action: true }); this.client.sound.play(Sound.CHANNEL_CREATED); }); diff --git a/shared/js/video-viewer/Controller.tsx b/shared/js/video-viewer/Controller.tsx index 4dec95dc..62547b95 100644 --- a/shared/js/video-viewer/Controller.tsx +++ b/shared/js/video-viewer/Controller.tsx @@ -16,21 +16,5 @@ const NumberRenderer = (props: { events: Registry }) => { export function spawnVideoPopout() { const registry = new Registry(); const modalController = spawnExternalModal("video-viewer", registry, {}); - modalController.open().then(() => { - const url = URL.createObjectURL(new Blob(["Hello World"], { type: "plain/text" })); - registry.fire("notify_data_url", { url: url }); - }); - - spawnReactModal(class extends Modal { - constructor() { - super(); - } - - title() { - return "Hello World"; - } - renderBody() { - return

Hello World:

; - } - }).show(); + modalController.open(); } \ No newline at end of file diff --git a/shared/js/video-viewer/Renderer.scss b/shared/js/video-viewer/Renderer.scss new file mode 100644 index 00000000..73e68206 --- /dev/null +++ b/shared/js/video-viewer/Renderer.scss @@ -0,0 +1,15 @@ +.container { + background: #19191b; + + display: flex; + flex-direction: row; + justify-content: stretch; + + flex-grow: 1; + flex-shrink: 1; + + min-height: 10em; + min-width: 20em; + + padding-left: 4em; +} \ No newline at end of file diff --git a/shared/js/video-viewer/Renderer.tsx b/shared/js/video-viewer/Renderer.tsx index 2a056b3d..a91e9e4d 100644 --- a/shared/js/video-viewer/Renderer.tsx +++ b/shared/js/video-viewer/Renderer.tsx @@ -4,7 +4,9 @@ import * as React from "react"; import {Registry} from "tc-shared/events"; import {VideoViewerEvents} from "./Definitions"; import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; -import {Slider} from "tc-shared/ui/react-elements/Slider"; +import ReactPlayer from 'react-player' + +const cssStyle = require("./Renderer.scss"); class ModalVideoPopout extends AbstractModal { readonly events: Registry; @@ -28,8 +30,31 @@ class ModalVideoPopout extends AbstractModal { } renderBody(): React.ReactElement { - return
- this.events.fire("notify_value", { value: value })} /> + return
+ console.log("onError(%o, %o, %o, %o)", error, data, hlsInstance, hlsGlobal)} + onBuffer={() => console.log("onBuffer()")} + onBufferEnd={() => console.log("onBufferEnd()")} + onDisablePIP={() => console.log("onDisabledPIP()")} + onEnablePIP={() => console.log("onEnablePIP()")} + onDuration={duration => console.log("onDuration(%o)", duration)} + onEnded={() => console.log("onEnded()")} + onPause={() => console.log("onPause()")} + onPlay={() => console.log("onPlay()")} + onProgress={state => console.log("onProgress(%o)", state)} + onReady={() => console.log("onReady()")} + onSeek={seconds => console.log("onSeek(%o)", seconds)} + onStart={() => console.log("onStart()")} + + controls={true} + + loop={false} + light={false} + />
; } } diff --git a/web/js/connection/ServerConnection.ts b/web/js/connection/ServerConnection.ts index d299076f..3277fa3b 100644 --- a/web/js/connection/ServerConnection.ts +++ b/web/js/connection/ServerConnection.ts @@ -15,9 +15,9 @@ import * as log from "tc-shared/log"; import {LogCategory} from "tc-shared/log"; import {Regex} from "tc-shared/ui/modal/ModalConnect"; import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler"; -import * as elog from "tc-shared/ui/frames/server_log"; import {VoiceConnection} from "../voice/VoiceHandler"; import AbstractVoiceConnection = voice.AbstractVoiceConnection; +import {EventType} from "tc-shared/ui/frames/log/Definitions"; class ReturnListener { resolve: (value?: T | PromiseLike) => void; @@ -290,7 +290,7 @@ export class ServerConnection extends AbstractServerConnection { private start_handshake() { this.updateConnectionState(ConnectionState.INITIALISING); - this.client.log.log(elog.Type.CONNECTION_LOGIN, {}); + this.client.log.log(EventType.CONNECTION_LOGIN, {}); this._handshakeHandler.initialize(); this._handshakeHandler.startHandshake(); } diff --git a/web/js/voice/VoiceHandler.ts b/web/js/voice/VoiceHandler.ts index 2f393eae..11bb969d 100644 --- a/web/js/voice/VoiceHandler.ts +++ b/web/js/voice/VoiceHandler.ts @@ -2,7 +2,6 @@ import * as log from "tc-shared/log"; import {LogCategory} from "tc-shared/log"; import * as loader from "tc-loader"; import * as aplayer from "../audio/player"; -import * as elog from "tc-shared/ui/frames/server_log"; import {BasicCodec} from "../codec/BasicCodec"; import {CodecType} from "../codec/Codec"; import {createErrorModal} from "tc-shared/ui/elements/Modal"; @@ -16,6 +15,7 @@ import {CallbackInputConsumer, InputConsumerType, NodeInputConsumer} from "tc-sh import AbstractVoiceConnection = voice.AbstractVoiceConnection; import VoiceClient = voice.VoiceClient; import {tr} from "tc-shared/i18n/localize"; +import {EventType} from "tc-shared/ui/frames/log/Definitions"; export namespace codec { class CacheEntry { @@ -508,7 +508,7 @@ export class VoiceConnection extends AbstractVoiceConnection { } else if(json["request"] == "status") { if(json["state"] == "failed") { const chandler = this.connection.client; - chandler.log.log(elog.Type.CONNECTION_VOICE_SETUP_FAILED, { + chandler.log.log(EventType.CONNECTION_VOICE_SETUP_FAILED, { reason: json["reason"], reconnect_delay: json["allow_reconnect"] ? 1 : 0 });