Added a basic notification system (Settings UI needs to be developed)

canary
WolverinDEV 2020-07-22 00:55:28 +02:00
parent ba7e618c19
commit 234e6d66b0
26 changed files with 1842 additions and 700 deletions

View File

@ -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

View File

@ -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", {

View File

@ -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);
}

View File

@ -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"
});
})
}
});

View File

@ -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
},

View File

@ -536,6 +536,13 @@ export class Settings extends StaticSettings {
}
};
static readonly FN_EVENTS_NOTIFICATION_ENABLED: (event: string) => SettingsKey<boolean> = event => {
return {
key: "notification_" + event + "_enabled",
valueType: "boolean"
}
};
static readonly KEYS = (() => {
const result = [];

View File

@ -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";
@ -101,3 +101,7 @@ export function renderBBCodeAsJQuery(message: string, settings: BBCodeRenderOpti
return [container.contents() as JQuery];
}
export function renderBBCodeAsText(message: string) {
return parseBBCode(message, { tag_whitelist: allowedBBCodes }).map(e => rendererText.render(e)).join("");
}

View File

@ -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<ChannelEvents> {
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<ChannelEvents> {
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

View File

@ -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<ClientEvents> {
callback: () => {
createInputModal(tr("Poke client"), tr("Poke message:<br>"), 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<ClientEvents> {
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<ClientEvents> {
}
}
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;

View File

@ -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 {

View File

@ -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();

View File

@ -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<EventType extends keyof TypeInfo> {
log(data: TypeInfo[EventType], logger: ServerEventLog) : React.ReactNode;
notify(data: TypeInfo[EventType], logger: ServerEventLog);
sound(data: TypeInfo[EventType], logger: ServerEventLog);
}
export interface ServerLogUIEvents {
"query_log": {},
"notify_log": {
log: LogMessage[]
},
"notify_log_add": {
event: LogMessage
}
}

View File

@ -0,0 +1,5 @@
.clientEntry, .channelEntry {
color: var(--server-log-tree-entry);
font-weight: 700;
cursor: pointer;
}

View File

@ -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<T extends keyof TypeInfo> = (data: TypeInfo[T], handlerId: string, eventType: T) => React.ReactNode;
const dispatchers: {[key: string]: DispatcherLog<any>} = { };
function registerDispatcher<T extends keyof TypeInfo>(key: T, builder: DispatcherLog<T>) {
dispatchers[key] = builder;
}
export function findLogDispatcher<T extends keyof TypeInfo>(type: T) : DispatcherLog<T> {
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 }) => (
<div className={cssStyle.clientEntry}>
{props.client.client_name}
</div>
);
/* TODO: Enable context menu */
const ChannelRenderer = (props: { channel: EventChannelData, handlerId: string, braces?: boolean }) => (
<div className={cssStyle.channelEntry}>
{props.channel.channel_name}
</div>
);
registerDispatcher(EventType.ERROR_CUSTOM, data => <div className={cssStyleRenderer.errorMessage}>{data.message}</div>);
registerDispatcher(EventType.CONNECTION_BEGIN, data => (
<VariadicTranslatable text={"Connecting to {0}{1}"}>
<>{data.address.server_hostname}</>
<>{data.address.server_port == 9987 ? "" : (":" + data.address.server_port)}</>
</VariadicTranslatable>
));
registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVE, () => (
<Translatable>Resolving hostname</Translatable>
));
registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVED, data => (
<VariadicTranslatable text={"Hostname resolved successfully to {0}:{1}"}>
<>{data.address.server_hostname}</>
<>{data.address.server_port}</>
</VariadicTranslatable>
));
registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVE_ERROR, data => (
<VariadicTranslatable text={"Failed to resolve hostname. Connecting to given hostname. Error: {0}"}>
<>{data.message}</>
</VariadicTranslatable>
));
registerDispatcher(EventType.CONNECTION_LOGIN, () => (
<Translatable>Logging in...</Translatable>
));
registerDispatcher(EventType.CONNECTION_FAILED, () => (
<Translatable>Connect failed.</Translatable>
));
registerDispatcher(EventType.CONNECTION_CONNECTED, (data,handlerId) => (
<VariadicTranslatable text={"Connected as {0}"}>
<ClientRenderer client={data.own_client} handlerId={handlerId} />
</VariadicTranslatable>
));
registerDispatcher(EventType.CONNECTION_VOICE_SETUP_FAILED, (data) => (
<VariadicTranslatable text={"Failed to setup voice bridge: {0}. Allow reconnect: {1}"}>
<>{data.reason}</>
{data.reconnect_delay > 0 ? <Translatable>Yes</Translatable> : <Translatable>No</Translatable>}
</VariadicTranslatable>
));
registerDispatcher(EventType.ERROR_PERMISSION, data => (
<div className={cssStyleRenderer.errorMessage}>
<VariadicTranslatable text={"Insufficient client permissions. Failed on permission {0}"}>
<>{data.permission ? data.permission.name : <Translatable>unknown</Translatable>}</>
</VariadicTranslatable>
</div>
));
registerDispatcher(EventType.CLIENT_VIEW_ENTER, (data, handlerId) => {
switch (data.reason) {
case ViewReasonId.VREASON_USER_ACTION:
if(data.channel_from) {
return (
<VariadicTranslatable text={"{0} appeared from {1} to {2}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
</VariadicTranslatable>
);
} else {
return (
<VariadicTranslatable text={"{0} appeared to channel {1}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
</VariadicTranslatable>
);
}
case ViewReasonId.VREASON_MOVED:
if(data.channel_from) {
return (
<VariadicTranslatable text={"{0} appeared from {1} to {2}, moved by {3}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
</VariadicTranslatable>
);
} else {
return (
<VariadicTranslatable text={"{0} appeared to {1}, moved by {2}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
</VariadicTranslatable>
);
}
case ViewReasonId.VREASON_CHANNEL_KICK:
if(data.channel_from) {
return (
<VariadicTranslatable text={"{0} appeared from {1} to {2}, kicked by {3}{4}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
<>{data.message ? " (" + data.message + ")" : ""}</>
</VariadicTranslatable>
);
} else {
return (
<VariadicTranslatable text={"{0} appeared to {1}, kicked by {2}{3}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
<>{data.message ? " (" + data.message + ")" : ""}</>
</VariadicTranslatable>
);
}
case ViewReasonId.VREASON_SYSTEM:
return undefined;
default:
return (
<div className={cssStyleRenderer.errorMessage}>
<VariadicTranslatable text={"Having user enter event with invalid reason: {0}"}>
<>{data.reason}</>
</VariadicTranslatable>
</div>
);
}
});
registerDispatcher(EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL, (data, handlerId) => {
switch (data.reason) {
case ViewReasonId.VREASON_USER_ACTION:
if(data.channel_from) {
return (
<VariadicTranslatable text={"{0} appeared from {1} to your channel {2}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
</VariadicTranslatable>
);
} else {
return (
<VariadicTranslatable text={"{0} appeared to your channel {1}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
</VariadicTranslatable>
);
}
case ViewReasonId.VREASON_MOVED:
if(data.channel_from) {
return (
<VariadicTranslatable text={"{0} appeared from {1} to your channel {2}, moved by {3}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
</VariadicTranslatable>
);
} else {
return (
<VariadicTranslatable text={"{0} appeared to your channel {1}, moved by {2}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
</VariadicTranslatable>
);
}
case ViewReasonId.VREASON_CHANNEL_KICK:
if(data.channel_from) {
return (
<VariadicTranslatable text={"{0} appeared from {1} to your channel {2}, kicked by {3}{4}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
<>{data.message ? " (" + data.message + ")" : ""}</>
</VariadicTranslatable>
);
} else {
return (
<VariadicTranslatable text={"{0} appeared to your channel {1}, kicked by {2}{3}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
<>{data.message ? " (" + data.message + ")" : ""}</>
</VariadicTranslatable>
);
}
case ViewReasonId.VREASON_SYSTEM:
return undefined;
default:
return (
<div className={cssStyleRenderer.errorMessage}>
<VariadicTranslatable text={"Having user enter your channel event with invalid reason: {0}"}>
<>{data.reason}</>
</VariadicTranslatable>
</div>
);
}
});
registerDispatcher(EventType.CLIENT_VIEW_MOVE, (data, handlerId) => {
switch (data.reason) {
case ViewReasonId.VREASON_MOVED:
return (
<VariadicTranslatable text={"{0} was moved from channel {1} to {2} by {3}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
</VariadicTranslatable>
);
case ViewReasonId.VREASON_USER_ACTION:
return (
<VariadicTranslatable text={"{0} switched from channel {1} to {2}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
</VariadicTranslatable>
);
case ViewReasonId.VREASON_CHANNEL_KICK:
return (
<VariadicTranslatable text={"{0} got kicked from channel {1} to {2} by {3}{4}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
<>{data.message ? " (" + data.message + ")" : ""}</>
</VariadicTranslatable>
);
default:
return (
<div className={cssStyleRenderer.errorMessage}>
<VariadicTranslatable text={"Having user move event with invalid reason: {0}"}>
<>{data.reason}</>
</VariadicTranslatable>
</div>
);
}
});
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 (
<VariadicTranslatable text={"You have been moved by {3} from channel {1} to {2}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
</VariadicTranslatable>
);
case ViewReasonId.VREASON_USER_ACTION:
return (
<VariadicTranslatable text={"You switched from {1} to {2}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
</VariadicTranslatable>
);
case ViewReasonId.VREASON_CHANNEL_KICK:
return (
<VariadicTranslatable text={"You got kicked out of the channel {1} to channel {2} by {3}{4}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
<>{data.message ? " (" + data.message + ")" : ""}</>
</VariadicTranslatable>
);
default:
return (
<div className={cssStyleRenderer.errorMessage}>
<VariadicTranslatable text={"Having own move event with invalid reason: {0}"}>
<>{data.reason}</>
</VariadicTranslatable>
</div>
);
}
});
registerDispatcher(EventType.CLIENT_VIEW_LEAVE, (data, handlerId) => {
switch (data.reason) {
case ViewReasonId.VREASON_USER_ACTION:
return (
<VariadicTranslatable text={"{0} disappeared from {1} to {2}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
</VariadicTranslatable>
);
case ViewReasonId.VREASON_SERVER_LEFT:
return (
<VariadicTranslatable text={"{0} left the server{1}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<>{data.message ? " (" + data.message + ")" : ""}</>
</VariadicTranslatable>
);
case ViewReasonId.VREASON_SERVER_KICK:
return (
<VariadicTranslatable text={"{0} was kicked from the server by {1}.{2}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
<>{data.message ? " (" + data.message + ")" : ""}</>
</VariadicTranslatable>
);
case ViewReasonId.VREASON_CHANNEL_KICK:
return (
<VariadicTranslatable text={"{0} was kicked from channel {1} by {2}.{3}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
<>{data.message ? " (" + data.message + ")" : ""}</>
</VariadicTranslatable>
);
case ViewReasonId.VREASON_BAN:
let duration = <Translatable>permanently</Translatable>;
if(data.ban_time)
duration = <VariadicTranslatable text={"for"}><>{" " + formatDate(data.ban_time)}</></VariadicTranslatable>;
return (
<VariadicTranslatable text={"{0} was banned {1} by {2}.{3}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
{duration}
<ClientRenderer client={data.invoker} handlerId={handlerId} />
<>{data.message ? " (" + data.message + ")" : ""}</>
</VariadicTranslatable>
);
case ViewReasonId.VREASON_TIMEOUT:
return (
<VariadicTranslatable text={"{0} timed out{1}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<>{data.message ? " (" + data.message + ")" : ""}</>
</VariadicTranslatable>
);
case ViewReasonId.VREASON_MOVED:
return (
<VariadicTranslatable text={"{0} disappeared from {1} to {2}, moved by {3}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
</VariadicTranslatable>
);
default:
return (
<div className={cssStyleRenderer.errorMessage}>
<VariadicTranslatable text={"Having client leave event with invalid reason: {0}"}>
<>{data.reason}</>
</VariadicTranslatable>
</div>
);
}
});
registerDispatcher(EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL, (data, handlerId) => {
switch (data.reason) {
case ViewReasonId.VREASON_USER_ACTION:
return (
<VariadicTranslatable text={"{0} disappeared from your channel {1} to {2}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
</VariadicTranslatable>
);
case ViewReasonId.VREASON_MOVED:
return (
<VariadicTranslatable text={"{0} disappeared from your channel {1} to {2}, moved by {3}"}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_from} handlerId={handlerId} />
<ChannelRenderer channel={data.channel_to} handlerId={handlerId} />
<ClientRenderer client={data.invoker} handlerId={handlerId} />
</VariadicTranslatable>
);
default:
return findLogDispatcher(EventType.CLIENT_VIEW_LEAVE)(data, handlerId, EventType.CLIENT_VIEW_LEAVE);
}
});
registerDispatcher(EventType.SERVER_WELCOME_MESSAGE,data => (
<BBCodeRenderer message={"[color=green]" + data.message + "[/color]"} settings={{convertSingleUrls: false}} />
));
registerDispatcher(EventType.SERVER_HOST_MESSAGE,data => (
<BBCodeRenderer message={"[color=green]" + data.message + "[/color]"} settings={{convertSingleUrls: false}} />
));
registerDispatcher(EventType.SERVER_HOST_MESSAGE_DISCONNECT,data => (
<BBCodeRenderer message={"[color=red]" + data.message + "[/color]"} settings={{convertSingleUrls: false}} />
));
registerDispatcher(EventType.CLIENT_NICKNAME_CHANGED,(data, handlerId) => (
<VariadicTranslatable text={"{0} changed his nickname from \"{1}\" to \"{2}\""}>
<ClientRenderer client={data.client} handlerId={handlerId} />
<>{data.old_name}</>
<>{data.new_name}</>
</VariadicTranslatable>
));
registerDispatcher(EventType.CLIENT_NICKNAME_CHANGED_OWN,() => (
<Translatable>Nickname successfully changed.</Translatable>
));
registerDispatcher(EventType.CLIENT_NICKNAME_CHANGE_FAILED,(data) => (
<VariadicTranslatable text={"Failed to change nickname: {0}"}>
<>{data.reason}</>
</VariadicTranslatable>
));
registerDispatcher(EventType.GLOBAL_MESSAGE, () => undefined);
registerDispatcher(EventType.DISCONNECTED,() => (
<Translatable>Disconnected from server</Translatable>
));
registerDispatcher(EventType.RECONNECT_SCHEDULED,data => (
<VariadicTranslatable text={"Reconnecting in {0}."}>
<>{format_time(data.timeout, tr("now"))}</>
</VariadicTranslatable>
));
registerDispatcher(EventType.RECONNECT_CANCELED,() => (
<Translatable>Reconnect canceled.</Translatable>
));
registerDispatcher(EventType.RECONNECT_CANCELED,() => (
<Translatable>Reconnecting...</Translatable>
));
registerDispatcher(EventType.SERVER_BANNED,(data, handlerId) => {
const time = data.time === 0 ? <Translatable>ever</Translatable> : <>{format_time(data.time * 1000, tr("one second"))}</>;
const reason = data.message ? <> <Translatable>Reason:</Translatable>&nbsp;{data.message}</> : undefined;
if(data.invoker.client_id > 0)
return (
<div className={cssStyleRenderer.errorMessage}>
<VariadicTranslatable text={"You've been banned from the server by {0} for {1}.{2}"}>
<ClientRenderer client={data.invoker} handlerId={handlerId} />
{time}
{reason}
</VariadicTranslatable>
</div>
);
else
return (
<div className={cssStyleRenderer.errorMessage}>
<VariadicTranslatable text={"You've been banned from the server for {0}.{1}"}>
{time}
{reason}
</VariadicTranslatable>
</div>
);
});
registerDispatcher(EventType.SERVER_REQUIRES_PASSWORD,() => (
<Translatable>Server requires a password to connect.</Translatable>
));
registerDispatcher(EventType.SERVER_CLOSED,data => {
if(data.message)
return (
<VariadicTranslatable text={"Server has been closed ({})."}>
<>{data.message}</>
</VariadicTranslatable>
);
return <Translatable>Server has been closed.</Translatable>;
});
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 (
<div className={cssStyleRenderer.errorMessage}>
<VariadicTranslatable text={"Command execution resulted in: {}"}>
<>{message}</>
</VariadicTranslatable>
</div>
)
});
registerDispatcher(EventType.CHANNEL_CREATE,(data, handlerId) => (
<VariadicTranslatable text={"Channel {} has been created by {}."}>
<ChannelRenderer channel={data.channel} handlerId={handlerId} />
<ClientRenderer client={data.creator} handlerId={handlerId} />
</VariadicTranslatable>
));
registerDispatcher(EventType.CHANNEL_CREATE_OWN,(data, handlerId) => (
<VariadicTranslatable text={"Channel {} has been created."}>
<ChannelRenderer channel={data.channel} handlerId={handlerId} />
</VariadicTranslatable>
));
registerDispatcher(EventType.CHANNEL_DELETE,(data, handlerId) => (
<VariadicTranslatable text={"Channel {} has been deleted by {}."}>
<ChannelRenderer channel={data.channel} handlerId={handlerId} />
<ClientRenderer client={data.deleter} handlerId={handlerId} />
</VariadicTranslatable>
));
registerDispatcher(EventType.CHANNEL_DELETE_OWN,(data, handlerId) => (
<VariadicTranslatable text={"Channel {} has been deleted."}>
<ChannelRenderer channel={data.channel} handlerId={handlerId} />
</VariadicTranslatable>
));
registerDispatcher(EventType.CLIENT_POKE_SEND,(data, handlerId) => (
<VariadicTranslatable text={"You poked {}."}>
<ClientRenderer client={data.target} handlerId={handlerId} />
</VariadicTranslatable>
));
registerDispatcher(EventType.CLIENT_POKE_RECEIVED,(data, handlerId) => {
if(data.message) {
return (
<VariadicTranslatable text={"You received a poke from {}: {}"}>
<ClientRenderer client={data.sender} handlerId={handlerId} />
<BBCodeRenderer message={data.message} settings={{ convertSingleUrls: false }} />
</VariadicTranslatable>
);
} else {
return (
<VariadicTranslatable text={"You received a poke from {}."}>
<ClientRenderer client={data.sender} handlerId={handlerId} />
</VariadicTranslatable>
);
}
});
registerDispatcher(EventType.PRIVATE_MESSAGE_RECEIVED, () => undefined);
registerDispatcher(EventType.PRIVATE_MESSAGE_SEND, () => undefined);

View File

@ -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<T extends keyof TypeInfo> = (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<any>} = { };
function registerDispatcher<T extends keyof TypeInfo>(key: T, builder: DispatcherLog<T>) {
dispatchers[key] = builder;
}
export function findNotificationDispatcher<T extends keyof TypeInfo>(type: T) : DispatcherLog<T> {
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
});

View File

@ -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);
}
}

View File

@ -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) => (
<div className={cssStyle.errorMessage}>
<VariadicTranslatable text={"Missing log entry builder for {0}"}>
<>{eventType}</>
</VariadicTranslatable>
</div>
);
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 (
<div className={cssStyle.logEntry}>
<div className={cssStyle.timestamp}>&lt;{
("0" + date.getHours()).substr(-2) + ":" +
("0" + date.getMinutes()).substr(-2) + ":" +
("0" + date.getSeconds()).substr(-2)
}&gt;</div>
{rendered}
</div>
);
});
export const ServerLogRenderer = (props: { events: Registry<ServerLogUIEvents>, handlerId: string }) => {
const [ logs, setLogs ] = useState<LogMessage[] | "loading">(() => {
props.events.fire_async("query_log");
return "loading";
});
const [ revision, setRevision ] = useState(0);
const refContainer = useRef<HTMLDivElement>();
const scrollOffset = useRef<number | "bottom">("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 (
<div className={cssStyle.logContainer} ref={refContainer} onScroll={event => {
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 => <LogEntryRenderer key={e.uniqueId} entry={e} handlerId={props.handlerId} />)}
</div>
);
};

View File

@ -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<ServerLogUIEvents>;
private htmlTag: HTMLDivElement;
private maxHistoryLength: number = 100;
private eventLog: LogMessage[] = [];
constructor(connection: ConnectionHandler) {
this.connection = connection;
this.uiEvents = new Registry<ServerLogUIEvents>();
this.htmlTag = document.createElement("div");
this.htmlTag.classList.add(cssStyle.htmlTag);
this.uiEvents.on("query_log", () => {
this.uiEvents.fire_async("notify_log", { log: this.eventLog });
});
ReactDOM.render(<ServerLogRenderer events={this.uiEvents} handlerId={this.connection.handlerId} />, this.htmlTag);
}
log<T extends keyof TypeInfo>(type: T, data: TypeInfo[T]) {
const event = {
data: data,
timestamp: Date.now(),
type: type as any,
uniqueId: "log-" + Date.now() + "-" + (++uniqueLogEventId)
};
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;
}
}

View File

@ -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<T extends keyof TypeInfo> = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | string | undefined;
export const MessageBuilders: {[key: string]: MessageBuilder<any>} = {
"error_custom": (data: event.ErrorCustom) => {
return [$.spawn("div").addClass("log-error").text(data.message)]
}
};
function register_message_builder<T extends keyof TypeInfo>(key: T, builder: MessageBuilder<T>) {
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<T extends keyof TypeInfo>(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));
});

View File

@ -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 <React.Fragment key={"argument-" + e + "-" + argsUseCount[e]}>{element}</React.Fragment>;
})
}
</>);
};
declare global {
interface Window {

View File

@ -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<ChannelEntry>(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);
});

View File

@ -16,21 +16,5 @@ const NumberRenderer = (props: { events: Registry<VideoViewerEvents> }) => {
export function spawnVideoPopout() {
const registry = new Registry<VideoViewerEvents>();
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 <h1>Hello World: <NumberRenderer events={registry} /></h1>;
}
}).show();
modalController.open();
}

View File

@ -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;
}

View File

@ -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<VideoViewerEvents>;
@ -28,8 +30,31 @@ class ModalVideoPopout extends AbstractModal {
}
renderBody(): React.ReactElement {
return <div style={{ padding: "10em" }}>
<Slider value={100} minValue={0} maxValue={100} stepSize={1} onInput={value => this.events.fire("notify_value", { value: value })} />
return <div className={cssStyle.container} >
<ReactPlayer
url={"https://www.youtube.com/watch?v=u_TuibFg-GA"}
height={"100%"}
width={"100%"}
onError={(error, data, hlsInstance, hlsGlobal) => 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}
/>
</div>;
}
}

View File

@ -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<T> {
resolve: (value?: T | PromiseLike<T>) => 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();
}

View File

@ -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
});