A lot of updates and added a new feature
parent
001bececbe
commit
3412faf125
|
@ -1,4 +1,11 @@
|
|||
# Changelog:
|
||||
* **18.12.20**
|
||||
- Added the ability to send private messages to multiple clients
|
||||
- Channel client count now updates within the side bar header
|
||||
- The client now supports the new server channel sidebar mode (File transfer is in progress)
|
||||
- Correctly parsing and render `client://` urls
|
||||
- Updating the client talk status if the client has been moved
|
||||
|
||||
* **13.12.20**
|
||||
- Directly connection when hitting enter on the address line
|
||||
|
||||
|
|
|
@ -134,6 +134,8 @@ html:root {
|
|||
min-height: 0;
|
||||
width: 100%;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
border-radius: 5px 5px 0 0;
|
||||
|
||||
padding-right: 5px;
|
||||
|
|
|
@ -24,8 +24,6 @@ import {FileTransferState, TransferProvider} from "./file/Transfer";
|
|||
import {traj, tr} from "./i18n/localize";
|
||||
import {md5} from "./crypto/md5";
|
||||
import {guid} from "./crypto/uid";
|
||||
import {ServerEventLog} from "./ui/frames/log/ServerEventLog";
|
||||
import {EventType} from "./ui/frames/log/Definitions";
|
||||
import {PluginCmdRegistry} from "./connection/PluginCmdHandler";
|
||||
import {W2GPluginCmdHandler} from "./video-viewer/W2GPlugin";
|
||||
import {VoiceConnectionStatus, WhisperSessionInitializeData} from "./connection/VoiceConnection";
|
||||
|
@ -41,6 +39,8 @@ import {ChannelConversationManager} from "./conversations/ChannelConversationMan
|
|||
import {PrivateConversationManager} from "tc-shared/conversations/PrivateConversationManager";
|
||||
import {SelectedClientInfo} from "./SelectedClientInfo";
|
||||
import {SideBarManager} from "tc-shared/SideBarManager";
|
||||
import {ServerEventLog} from "tc-shared/connectionlog/ServerEventLog";
|
||||
import {EventType} from "tc-shared/connectionlog/Definitions";
|
||||
|
||||
export enum InputHardwareState {
|
||||
MISSING,
|
||||
|
@ -270,7 +270,7 @@ export class ConnectionHandler {
|
|||
}
|
||||
}
|
||||
log.info(LogCategory.CLIENT, tr("Start connection to %s:%d"), server_address.host, server_address.port);
|
||||
this.log.log(EventType.CONNECTION_BEGIN, {
|
||||
this.log.log("connection.begin", {
|
||||
address: {
|
||||
server_hostname: server_address.host,
|
||||
server_port: server_address.port
|
||||
|
@ -303,7 +303,7 @@ export class ConnectionHandler {
|
|||
server_address.host = "127.0.0.1";
|
||||
} else if(dns.supported() && !server_address.host.match(Regex.IP_V4) && !server_address.host.match(Regex.IP_V6)) {
|
||||
const id = ++this._connect_initialize_id;
|
||||
this.log.log(EventType.CONNECTION_HOSTNAME_RESOLVE, {});
|
||||
this.log.log("connection.hostname.resolve", {});
|
||||
try {
|
||||
const resolved = await dns.resolve_address(server_address, { timeout: 5000 }) || {} as any;
|
||||
if(id != this._connect_initialize_id)
|
||||
|
@ -311,7 +311,7 @@ export class ConnectionHandler {
|
|||
|
||||
server_address.host = typeof(resolved.target_ip) === "string" ? resolved.target_ip : server_address.host;
|
||||
server_address.port = typeof(resolved.target_port) === "number" ? resolved.target_port : server_address.port;
|
||||
this.log.log(EventType.CONNECTION_HOSTNAME_RESOLVED, {
|
||||
this.log.log("connection.hostname.resolved", {
|
||||
address: {
|
||||
server_port: server_address.port,
|
||||
server_hostname: server_address.host
|
||||
|
@ -348,7 +348,7 @@ export class ConnectionHandler {
|
|||
log.warn(LogCategory.CLIENT, tr("Failed to successfully disconnect from server: {}"), error);
|
||||
}
|
||||
this.sound.play(Sound.CONNECTION_DISCONNECTED);
|
||||
this.log.log(EventType.DISCONNECTED, {});
|
||||
this.log.log("disconnected", {});
|
||||
}
|
||||
|
||||
getClient() : LocalClientEntry { return this.localClient; }
|
||||
|
@ -386,7 +386,7 @@ export class ConnectionHandler {
|
|||
this.connection_state = event.newState;
|
||||
if(event.newState === ConnectionState.CONNECTED) {
|
||||
log.info(LogCategory.CLIENT, tr("Client connected"));
|
||||
this.log.log(EventType.CONNECTION_CONNECTED, {
|
||||
this.log.log("connection.connected", {
|
||||
serverAddress: {
|
||||
server_port: this.channelTree.server.remote_address.port,
|
||||
server_hostname: this.channelTree.server.remote_address.host
|
||||
|
@ -487,12 +487,12 @@ export class ConnectionHandler {
|
|||
case DisconnectReason.HANDLER_DESTROYED:
|
||||
if(data) {
|
||||
this.sound.play(Sound.CONNECTION_DISCONNECTED);
|
||||
this.log.log(EventType.DISCONNECTED, {});
|
||||
this.log.log("disconnected", {});
|
||||
}
|
||||
break;
|
||||
case DisconnectReason.DNS_FAILED:
|
||||
log.error(LogCategory.CLIENT, tr("Failed to resolve hostname: %o"), data);
|
||||
this.log.log(EventType.CONNECTION_HOSTNAME_RESOLVE_ERROR, {
|
||||
this.log.log("connection.hostname.resolve.error", {
|
||||
message: data as any
|
||||
});
|
||||
this.sound.play(Sound.CONNECTION_REFUSED);
|
||||
|
@ -539,7 +539,7 @@ export class ConnectionHandler {
|
|||
this._certificate_modal.open();
|
||||
});
|
||||
}
|
||||
this.log.log(EventType.CONNECTION_FAILED, {
|
||||
this.log.log("connection.failed", {
|
||||
serverAddress: {
|
||||
server_hostname: this.serverConnection.remote_address().host,
|
||||
server_port: this.serverConnection.remote_address().port
|
||||
|
@ -594,7 +594,7 @@ export class ConnectionHandler {
|
|||
|
||||
break;
|
||||
case DisconnectReason.SERVER_CLOSED:
|
||||
this.log.log(EventType.SERVER_CLOSED, {message: data.reasonmsg});
|
||||
this.log.log("server.closed", {message: data.reasonmsg});
|
||||
|
||||
createErrorModal(
|
||||
tr("Server closed"),
|
||||
|
@ -606,7 +606,7 @@ export class ConnectionHandler {
|
|||
auto_reconnect = true;
|
||||
break;
|
||||
case DisconnectReason.SERVER_REQUIRES_PASSWORD:
|
||||
this.log.log(EventType.SERVER_REQUIRES_PASSWORD, {});
|
||||
this.log.log("server.requires.password", {});
|
||||
|
||||
createInputModal(tr("Server password"), tr("Enter server password:"), password => password.length != 0, password => {
|
||||
if(!(typeof password === "string")) return;
|
||||
|
@ -647,7 +647,7 @@ export class ConnectionHandler {
|
|||
this.sound.play(Sound.CONNECTION_BANNED);
|
||||
break;
|
||||
case DisconnectReason.CLIENT_BANNED:
|
||||
this.log.log(EventType.SERVER_BANNED, {
|
||||
this.log.log("server.banned", {
|
||||
invoker: {
|
||||
client_name: data["invokername"],
|
||||
client_id: parseInt(data["invokerid"]),
|
||||
|
@ -678,7 +678,7 @@ export class ConnectionHandler {
|
|||
log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect but cant reconnect because we dont have any information left..."));
|
||||
return;
|
||||
}
|
||||
this.log.log(EventType.RECONNECT_SCHEDULED, {timeout: 5000});
|
||||
this.log.log("reconnect.scheduled", {timeout: 5000});
|
||||
|
||||
log.info(LogCategory.NETWORKING, tr("Allowed to auto reconnect. Reconnecting in 5000ms"));
|
||||
const server_address = this.serverConnection.remote_address();
|
||||
|
@ -686,7 +686,7 @@ export class ConnectionHandler {
|
|||
|
||||
this._reconnect_timer = setTimeout(() => {
|
||||
this._reconnect_timer = undefined;
|
||||
this.log.log(EventType.RECONNECT_EXECUTE, {});
|
||||
this.log.log("reconnect.execute", {});
|
||||
log.info(LogCategory.NETWORKING, tr("Reconnecting..."));
|
||||
|
||||
this.startConnection(server_address.host + ":" + server_address.port, profile, false, Object.assign(this.reconnect_properties(profile), {auto_reconnect_attempt: true}));
|
||||
|
@ -698,7 +698,7 @@ export class ConnectionHandler {
|
|||
|
||||
cancel_reconnect(log_event: boolean) {
|
||||
if(this._reconnect_timer) {
|
||||
if(log_event) this.log.log(EventType.RECONNECT_CANCELED, {});
|
||||
if(log_event) this.log.log("reconnect.canceled", {});
|
||||
clearTimeout(this._reconnect_timer);
|
||||
this._reconnect_timer = undefined;
|
||||
}
|
||||
|
@ -791,7 +791,7 @@ export class ConnectionHandler {
|
|||
this.clientStatusSync = true;
|
||||
this.serverConnection.send_command("clientupdate", localClientUpdates).catch(error => {
|
||||
log.warn(LogCategory.GENERAL, tr("Failed to update client audio hardware properties. Error: %o"), error);
|
||||
this.log.log(EventType.ERROR_CUSTOM, { message: tr("Failed to update audio hardware properties.") });
|
||||
this.log.log("error.custom", { message: tr("Failed to update audio hardware properties.") });
|
||||
this.clientStatusSync = false;
|
||||
});
|
||||
}
|
||||
|
@ -837,7 +837,7 @@ export class ConnectionHandler {
|
|||
//client_output_hardware: this.client_status.sound_playback_supported
|
||||
}).catch(error => {
|
||||
log.warn(LogCategory.GENERAL, tr("Failed to sync handler state with server. Error: %o"), error);
|
||||
this.log.log(EventType.ERROR_CUSTOM, {message: tr("Failed to sync handler state with server.")});
|
||||
this.log.log("error.custom", {message: tr("Failed to sync handler state with server.")});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1154,7 +1154,7 @@ export class ConnectionHandler {
|
|||
client_away_message: typeof(this.client_status.away) === "string" ? this.client_status.away : "",
|
||||
}).catch(error => {
|
||||
log.warn(LogCategory.GENERAL, tr("Failed to update away status. Error: %o"), error);
|
||||
this.log.log(EventType.ERROR_CUSTOM, {message: tr("Failed to update away status.")});
|
||||
this.log.log("error.custom", {message: tr("Failed to update away status.")});
|
||||
});
|
||||
|
||||
this.event_registry.fire("notify_state_updated", {
|
||||
|
|
|
@ -6,6 +6,8 @@ import {FooterRenderer} from "tc-shared/ui/frames/footer/Renderer";
|
|||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import {SideBarController} from "tc-shared/ui/frames/SideBarController";
|
||||
import {ServerEventLogController} from "tc-shared/ui/frames/log/Controller";
|
||||
import {ServerLogFrame} from "tc-shared/ui/frames/log/Renderer";
|
||||
|
||||
export let server_connections: ConnectionManager;
|
||||
|
||||
|
@ -30,33 +32,36 @@ export class ConnectionManager {
|
|||
private connection_handlers: ConnectionHandler[] = [];
|
||||
private active_handler: ConnectionHandler | undefined;
|
||||
|
||||
private _container_log_server: JQuery;
|
||||
private _container_channel_tree: JQuery;
|
||||
private _container_hostbanner: JQuery;
|
||||
private containerChannelVideo: ReplaceableContainer;
|
||||
private containerSideBar: HTMLDivElement;
|
||||
private containerFooter: HTMLDivElement;
|
||||
private containerServerLog: HTMLDivElement;
|
||||
|
||||
private sideBarController: SideBarController;
|
||||
private serverLogController: ServerEventLogController;
|
||||
|
||||
constructor() {
|
||||
this.event_registry = new Registry<ConnectionManagerEvents>();
|
||||
this.event_registry.enableDebug("connection-manager");
|
||||
|
||||
this.sideBarController = new SideBarController();
|
||||
this.serverLogController = new ServerEventLogController();
|
||||
|
||||
this.containerChannelVideo = new ReplaceableContainer(document.getElementById("channel-video") as HTMLDivElement);
|
||||
this._container_log_server = $("#server-log");
|
||||
this.containerServerLog = document.getElementById("server-log") as HTMLDivElement;
|
||||
this.containerFooter = document.getElementById("container-footer") as HTMLDivElement;
|
||||
this._container_channel_tree = $("#channelTree");
|
||||
this._container_hostbanner = $("#hostbanner");
|
||||
this.containerFooter = document.getElementById("container-footer") as HTMLDivElement;
|
||||
|
||||
this.sideBarController.renderInto(document.getElementById("chat") as HTMLDivElement);
|
||||
this.set_active_connection(undefined);
|
||||
}
|
||||
|
||||
initializeFooter() {
|
||||
initializeReactComponents() {
|
||||
ReactDOM.render(React.createElement(FooterRenderer), this.containerFooter);
|
||||
ReactDOM.render(React.createElement(ServerLogFrame, { events: this.serverLogController.events }), this.containerServerLog);
|
||||
}
|
||||
|
||||
events() : Registry<ConnectionManagerEvents> {
|
||||
|
@ -117,16 +122,15 @@ export class ConnectionManager {
|
|||
|
||||
private set_active_connection_(handler: ConnectionHandler) {
|
||||
this.sideBarController.setConnection(handler);
|
||||
this.serverLogController.setConnectionHandler(handler);
|
||||
|
||||
this._container_channel_tree.children().detach();
|
||||
this._container_log_server.children().detach();
|
||||
this._container_hostbanner.children().detach();
|
||||
this.containerChannelVideo.replaceWith(handler?.video_frame.getContainer());
|
||||
|
||||
if(handler) {
|
||||
this._container_hostbanner.append(handler.hostbanner.html_tag);
|
||||
this._container_channel_tree.append(handler.channelTree.tag_tree());
|
||||
this._container_log_server.append(handler.log.getHTMLTag());
|
||||
}
|
||||
|
||||
const old_handler = this.active_handler;
|
||||
|
@ -184,7 +188,7 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
|||
name: "server manager init",
|
||||
function: async () => {
|
||||
server_connections = new ConnectionManager();
|
||||
server_connections.initializeFooter();
|
||||
server_connections.initializeReactComponents();
|
||||
},
|
||||
priority: 80
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ export class SideBarManager {
|
|||
constructor(connection: ConnectionHandler) {
|
||||
this.events = new Registry<SideBarManagerEvents>();
|
||||
this.connection = connection;
|
||||
this.currentType = "channel-chat";
|
||||
this.currentType = "channel";
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
|
@ -38,8 +38,8 @@ export class SideBarManager {
|
|||
this.setSideBarContent("private-chat");
|
||||
}
|
||||
|
||||
showChannelConversations() {
|
||||
this.setSideBarContent("channel-chat");
|
||||
showChannel() {
|
||||
this.setSideBarContent("channel");
|
||||
}
|
||||
|
||||
showClientInfo(client: ClientEntry) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as log from "../log";
|
||||
import {LogCategory, logError} from "../log";
|
||||
import {LogCategory, logError, logWarn} from "../log";
|
||||
import {AbstractServerConnection, CommandOptions, ServerCommand} from "../connection/ConnectionBase";
|
||||
import {Sound} from "../sound/Sounds";
|
||||
import {CommandResult} from "../connection/ServerConnectionDeclaration";
|
||||
|
@ -20,10 +20,10 @@ import {batch_updates, BatchUpdateType, flush_batched_updates} from "../ui/react
|
|||
import {OutOfViewClient} from "../ui/frames/side/PrivateConversationController";
|
||||
import {renderBBCodeAsJQuery} from "../text/bbcode";
|
||||
import {tr} from "../i18n/localize";
|
||||
import {EventClient, EventType} from "../ui/frames/log/Definitions";
|
||||
import {ErrorCode} from "../connection/ErrorCode";
|
||||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
import {ChannelEntry} from "tc-shared/tree/Channel";
|
||||
import {EventClient} from "tc-shared/connectionlog/Definitions";
|
||||
|
||||
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
|
||||
constructor(connection: AbstractServerConnection) {
|
||||
|
@ -56,6 +56,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
this["initserver"] = this.handleCommandServerInit;
|
||||
this["notifychannelmoved"] = this.handleNotifyChannelMoved;
|
||||
this["notifychanneledited"] = this.handleNotifyChannelEdited;
|
||||
this["notifychanneldescriptionchanged"] = this.handleNotifyChannelDescriptionChanged;
|
||||
this["notifytextmessage"] = this.handleNotifyTextMessage;
|
||||
this["notifyclientchatcomposing"] = this.notifyClientChatComposing;
|
||||
this["notifyclientchatclosed"] = this.handleNotifyClientChatClosed;
|
||||
|
@ -116,18 +117,18 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
if(res.id == ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) { //Permission error
|
||||
const permission = this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number);
|
||||
res.message = tr("Insufficient client permissions. Failed on permission ") + (permission ? permission.name : "unknown");
|
||||
this.connection_handler.log.log(EventType.ERROR_PERMISSION, {
|
||||
this.connection_handler.log.log("error.permission", {
|
||||
permission: this.connection_handler.permissions.resolveInfo(res.json["failed_permid"] as number)
|
||||
});
|
||||
this.connection_handler.sound.play(Sound.ERROR_INSUFFICIENT_PERMISSIONS);
|
||||
} else if(res.id != ErrorCode.DATABASE_EMPTY_RESULT) {
|
||||
this.connection_handler.log.log(EventType.ERROR_CUSTOM, {
|
||||
this.connection_handler.log.log("error.custom", {
|
||||
message: res.extra_message.length == 0 ? res.message : res.extra_message
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if(typeof(ex) === "string") {
|
||||
this.connection_handler.log.log(EventType.CONNECTION_COMMAND_ERROR, {error: ex});
|
||||
this.connection_handler.log.log("connection.command.error", {error: ex});
|
||||
} else {
|
||||
log.error(LogCategory.NETWORKING, tr("Invalid promise result type: %s. Result: %o"), typeof (ex), ex);
|
||||
}
|
||||
|
@ -204,7 +205,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
if(properties.virtualserver_hostmessage_mode == 1) {
|
||||
/* show in log */
|
||||
if(properties.virtualserver_hostmessage)
|
||||
this.connection_handler.log.log(EventType.SERVER_HOST_MESSAGE, {
|
||||
this.connection_handler.log.log("server.host.message", {
|
||||
message: properties.virtualserver_hostmessage
|
||||
});
|
||||
} else {
|
||||
|
@ -219,7 +220,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
if(properties.virtualserver_hostmessage_mode == 3) {
|
||||
/* first let the client initialize his stuff */
|
||||
setTimeout(() => {
|
||||
this.connection_handler.log.log(EventType.SERVER_HOST_MESSAGE_DISCONNECT, {
|
||||
this.connection_handler.log.log("server.host.message.disconnect", {
|
||||
message: properties.virtualserver_welcomemessage
|
||||
});
|
||||
|
||||
|
@ -233,7 +234,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
|
||||
/* welcome message */
|
||||
if(properties.virtualserver_welcomemessage) {
|
||||
this.connection_handler.log.log(EventType.SERVER_WELCOME_MESSAGE, {
|
||||
this.connection_handler.log.log("server.welcome.message", {
|
||||
message: properties.virtualserver_welcomemessage
|
||||
});
|
||||
}
|
||||
|
@ -504,7 +505,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
|
||||
if(this.connection_handler.areQueriesShown() || client.properties.client_type != ClientType.CLIENT_QUERY) {
|
||||
const own_channel = this.connection.client.getClient().currentChannel();
|
||||
this.connection_handler.log.log(channel == own_channel ? EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL : EventType.CLIENT_VIEW_ENTER, {
|
||||
this.connection_handler.log.log(channel == own_channel ? "client.view.enter.own.channel" : "client.view.enter", {
|
||||
channel_from: old_channel ? old_channel.log_data() : undefined,
|
||||
channel_to: channel ? channel.log_data() : undefined,
|
||||
client: client.log_data(),
|
||||
|
@ -592,7 +593,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
let channel_to = tree.findChannel(targetChannelId);
|
||||
|
||||
const is_own_channel = channel_from == own_channel;
|
||||
this.connection_handler.log.log(is_own_channel ? EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL : EventType.CLIENT_VIEW_LEAVE, {
|
||||
this.connection_handler.log.log(is_own_channel ? "client.view.leave.own.channel" : "client.view.leave", {
|
||||
channel_from: channel_from ? channel_from.log_data() : undefined,
|
||||
channel_to: channel_to ? channel_to.log_data() : undefined,
|
||||
client: client.log_data(),
|
||||
|
@ -673,7 +674,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
}
|
||||
|
||||
const own_channel = this.connection.client.getClient().currentChannel();
|
||||
const event = self ? EventType.CLIENT_VIEW_MOVE_OWN : (channelFrom == own_channel || channel_to == own_channel ? EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL : EventType.CLIENT_VIEW_MOVE);
|
||||
const event = self ? "client.view.move.own" : (channelFrom == own_channel || channel_to == own_channel ? "client.view.move.own.channel" : "client.view.move");
|
||||
this.connection_handler.log.log(event, {
|
||||
channel_from: channelFrom ? {
|
||||
channel_id: channelFrom.channelId,
|
||||
|
@ -779,6 +780,19 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
}
|
||||
}
|
||||
|
||||
handleNotifyChannelDescriptionChanged(json) {
|
||||
json = json[0];
|
||||
|
||||
let tree = this.connection.client.channelTree;
|
||||
let channel = tree.findChannel(parseInt(json["cid"]));
|
||||
if(!channel) {
|
||||
logWarn(LogCategory.NETWORKING, tr("Received channel description changed notify for invalid channel: %o"), json["cid"]);
|
||||
return;
|
||||
}
|
||||
|
||||
channel.handleDescriptionChanged();
|
||||
}
|
||||
|
||||
handleNotifyTextMessage(json) {
|
||||
json = json[0]; //Only one bulk
|
||||
|
||||
|
@ -815,7 +829,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
});
|
||||
if(targetIsOwn) {
|
||||
this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5});
|
||||
this.connection_handler.log.log(EventType.PRIVATE_MESSAGE_RECEIVED, {
|
||||
this.connection_handler.log.log("private.message.received", {
|
||||
message: json["msg"],
|
||||
sender: {
|
||||
client_unique_id: json["invokeruid"],
|
||||
|
@ -825,7 +839,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
});
|
||||
} else {
|
||||
this.connection_handler.sound.play(Sound.MESSAGE_SEND, {default_volume: .5});
|
||||
this.connection_handler.log.log(EventType.PRIVATE_MESSAGE_SEND, {
|
||||
this.connection_handler.log.log("private.message.send", {
|
||||
message: json["msg"],
|
||||
target: {
|
||||
client_unique_id: json["invokeruid"],
|
||||
|
@ -858,7 +872,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"]));
|
||||
const conversations = this.connection_handler.getChannelConversations();
|
||||
|
||||
this.connection_handler.log.log(EventType.GLOBAL_MESSAGE, {
|
||||
this.connection_handler.log.log("global.message", {
|
||||
isOwnMessage: invoker instanceof LocalClientEntry,
|
||||
message: json["msg"],
|
||||
sender: {
|
||||
|
@ -976,7 +990,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
unique_id: json["invokeruid"]
|
||||
}, json["msg"]);
|
||||
|
||||
this.connection_handler.log.log(EventType.CLIENT_POKE_RECEIVED, {
|
||||
this.connection_handler.log.log("client.poke.received", {
|
||||
sender: this.loggable_invoker(json["invokeruid"], json["invokerid"], json["invokername"]),
|
||||
message: json["msg"]
|
||||
});
|
||||
|
|
|
@ -172,6 +172,7 @@ export enum ErrorCode {
|
|||
|
||||
CUSTOM_ERROR = 0xFFFF,
|
||||
|
||||
/** @deprecated Use SERVER_INSUFFICIENT_PERMISSIONS */
|
||||
PERMISSION_ERROR = ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS,
|
||||
EMPTY_RESULT = ErrorCode.DATABASE_EMPTY_RESULT
|
||||
}
|
|
@ -0,0 +1,345 @@
|
|||
import * as React from "react";
|
||||
import {ServerEventLog} from "tc-shared/connectionlog/ServerEventLog";
|
||||
import {ViewReasonId} from "tc-shared/ConnectionHandler";
|
||||
import {PermissionInfo} from "tc-shared/permission/PermissionManager";
|
||||
|
||||
/* FIXME: Remove this! */
|
||||
export enum EventType {
|
||||
CONNECTION_BEGIN = "connection.begin",
|
||||
CONNECTION_HOSTNAME_RESOLVE = "connection.hostname.resolve",
|
||||
CONNECTION_HOSTNAME_RESOLVE_ERROR = "connection.hostname.resolve.error",
|
||||
CONNECTION_HOSTNAME_RESOLVED = "connection.hostname.resolved",
|
||||
CONNECTION_LOGIN = "connection.login",
|
||||
CONNECTION_CONNECTED = "connection.connected",
|
||||
CONNECTION_FAILED = "connection.failed",
|
||||
|
||||
DISCONNECTED = "disconnected",
|
||||
|
||||
CONNECTION_VOICE_CONNECT = "connection.voice.connect",
|
||||
CONNECTION_VOICE_CONNECT_FAILED = "connection.voice.connect.failed",
|
||||
CONNECTION_VOICE_CONNECT_SUCCEEDED = "connection.voice.connect.succeeded",
|
||||
CONNECTION_VOICE_DROPPED = "connection.voice.dropped",
|
||||
|
||||
CONNECTION_COMMAND_ERROR = "connection.command.error",
|
||||
|
||||
GLOBAL_MESSAGE = "global.message",
|
||||
|
||||
SERVER_WELCOME_MESSAGE = "server.welcome.message",
|
||||
SERVER_HOST_MESSAGE = "server.host.message",
|
||||
SERVER_HOST_MESSAGE_DISCONNECT = "server.host.message.disconnect",
|
||||
|
||||
SERVER_CLOSED = "server.closed",
|
||||
SERVER_BANNED = "server.banned",
|
||||
SERVER_REQUIRES_PASSWORD = "server.requires.password",
|
||||
|
||||
CLIENT_VIEW_ENTER = "client.view.enter",
|
||||
CLIENT_VIEW_LEAVE = "client.view.leave",
|
||||
CLIENT_VIEW_MOVE = "client.view.move",
|
||||
|
||||
CLIENT_VIEW_ENTER_OWN_CHANNEL = "client.view.enter.own.channel",
|
||||
CLIENT_VIEW_LEAVE_OWN_CHANNEL = "client.view.leave.own.channel",
|
||||
CLIENT_VIEW_MOVE_OWN_CHANNEL = "client.view.move.own.channel",
|
||||
|
||||
CLIENT_VIEW_MOVE_OWN = "client.view.move.own",
|
||||
|
||||
CLIENT_NICKNAME_CHANGED = "client.nickname.changed",
|
||||
CLIENT_NICKNAME_CHANGED_OWN = "client.nickname.changed.own",
|
||||
CLIENT_NICKNAME_CHANGE_FAILED = "client.nickname.change.failed",
|
||||
|
||||
CLIENT_SERVER_GROUP_ADD = "client.server.group.add",
|
||||
CLIENT_SERVER_GROUP_REMOVE = "client.server.group.remove",
|
||||
CLIENT_CHANNEL_GROUP_CHANGE = "client.channel.group.change",
|
||||
|
||||
PRIVATE_MESSAGE_RECEIVED = "private.message.received",
|
||||
PRIVATE_MESSAGE_SEND = "private.message.send",
|
||||
|
||||
CHANNEL_CREATE = "channel.create",
|
||||
CHANNEL_DELETE = "channel.delete",
|
||||
|
||||
ERROR_CUSTOM = "error.custom",
|
||||
ERROR_PERMISSION = "error.permission",
|
||||
|
||||
CLIENT_POKE_RECEIVED = "client.poke.received",
|
||||
CLIENT_POKE_SEND = "client.poke.send",
|
||||
|
||||
RECONNECT_SCHEDULED = "reconnect.scheduled",
|
||||
RECONNECT_EXECUTE = "reconnect.execute",
|
||||
RECONNECT_CANCELED = "reconnect.canceled",
|
||||
|
||||
WEBRTC_FATAL_ERROR = "webrtc.fatal.error"
|
||||
}
|
||||
|
||||
export type EventClient = {
|
||||
client_unique_id: string;
|
||||
client_name: string;
|
||||
client_id: number;
|
||||
}
|
||||
export type EventChannelData = {
|
||||
channel_id: number;
|
||||
channel_name: string;
|
||||
}
|
||||
|
||||
export type EventServerAddress = {
|
||||
server_hostname: string;
|
||||
server_port: number;
|
||||
}
|
||||
|
||||
export namespace event {
|
||||
export type EventGlobalMessage = {
|
||||
isOwnMessage: boolean;
|
||||
sender: EventClient;
|
||||
message: string;
|
||||
}
|
||||
export type EventConnectBegin = {
|
||||
address: EventServerAddress;
|
||||
client_nickname: string;
|
||||
}
|
||||
export type EventErrorCustom = {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type EventReconnectScheduled = {
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
export type EventReconnectCanceled = { }
|
||||
export type EventReconnectExecute = { }
|
||||
|
||||
export type EventErrorPermission = {
|
||||
permission: PermissionInfo;
|
||||
}
|
||||
|
||||
export type EventWelcomeMessage = {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type EventHostMessageDisconnect = {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type EventClientMove = {
|
||||
channel_from?: EventChannelData;
|
||||
channel_from_own: boolean;
|
||||
|
||||
channel_to?: EventChannelData;
|
||||
channel_to_own: boolean;
|
||||
|
||||
client: EventClient;
|
||||
client_own: boolean;
|
||||
|
||||
invoker?: EventClient;
|
||||
|
||||
message?: string;
|
||||
reason: ViewReasonId;
|
||||
}
|
||||
|
||||
export type EventClientEnter = {
|
||||
channel_from?: EventChannelData;
|
||||
channel_to?: EventChannelData;
|
||||
|
||||
client: EventClient;
|
||||
invoker?: EventClient;
|
||||
|
||||
message?: string;
|
||||
|
||||
reason: ViewReasonId;
|
||||
ban_time?: number;
|
||||
}
|
||||
|
||||
export type EventClientLeave = {
|
||||
channel_from?: EventChannelData;
|
||||
channel_to?: EventChannelData;
|
||||
|
||||
client: EventClient;
|
||||
invoker?: EventClient;
|
||||
|
||||
message?: string;
|
||||
|
||||
reason: ViewReasonId;
|
||||
ban_time?: number;
|
||||
}
|
||||
|
||||
export type EventChannelCreate = {
|
||||
creator: EventClient,
|
||||
channel: EventChannelData,
|
||||
ownAction: boolean
|
||||
}
|
||||
|
||||
export type EventChannelToggle = {
|
||||
channel: EventChannelData
|
||||
}
|
||||
|
||||
export type EventChannelDelete = {
|
||||
deleter: EventClient,
|
||||
channel: EventChannelData,
|
||||
ownAction: boolean
|
||||
}
|
||||
|
||||
export type EventConnectionConnected = {
|
||||
serverName: string,
|
||||
serverAddress: EventServerAddress,
|
||||
own_client: EventClient;
|
||||
}
|
||||
export type EventConnectionFailed = {
|
||||
serverAddress: EventServerAddress
|
||||
}
|
||||
export type EventConnectionLogin = {}
|
||||
export type EventConnectionHostnameResolve = {};
|
||||
export type EventConnectionHostnameResolved = {
|
||||
address: EventServerAddress;
|
||||
}
|
||||
export type EventConnectionHostnameResolveError = {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type EventConnectionVoiceConnectFailed = {
|
||||
reason: string;
|
||||
reconnect_delay: number; /* if less or equal to 0 reconnect is prohibited */
|
||||
}
|
||||
|
||||
export type EventConnectionVoiceConnectSucceeded = {}
|
||||
|
||||
export type EventConnectionVoiceConnect = {
|
||||
attemptCount: number
|
||||
}
|
||||
|
||||
export type EventConnectionVoiceDropped = {}
|
||||
|
||||
export type EventConnectionCommandError = {
|
||||
error: any;
|
||||
}
|
||||
|
||||
export type EventClientNicknameChanged = {
|
||||
client: EventClient;
|
||||
|
||||
old_name: string;
|
||||
new_name: string;
|
||||
}
|
||||
|
||||
export type EventClientNicknameChangeFailed = {
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export type EventServerClosed = {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type EventServerRequiresPassword = {}
|
||||
|
||||
export type EventServerBanned = {
|
||||
message: string;
|
||||
time: number;
|
||||
|
||||
invoker: EventClient;
|
||||
}
|
||||
|
||||
export type EventClientPokeReceived = {
|
||||
sender: EventClient,
|
||||
message: string
|
||||
}
|
||||
|
||||
export type EventClientPokeSend = {
|
||||
target: EventClient,
|
||||
message: string
|
||||
}
|
||||
|
||||
export type EventPrivateMessageSend = {
|
||||
target: EventClient,
|
||||
message: string
|
||||
}
|
||||
|
||||
export type EventPrivateMessageReceived = {
|
||||
sender: EventClient,
|
||||
message: string
|
||||
}
|
||||
|
||||
export type EventWebrtcFatalError = {
|
||||
message: string,
|
||||
retryTimeout: number | 0
|
||||
}
|
||||
}
|
||||
|
||||
export type LogMessage = {
|
||||
type: EventType;
|
||||
uniqueId: string;
|
||||
timestamp: number;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export interface TypeInfo {
|
||||
"connection.begin" : event.EventConnectBegin;
|
||||
"global.message": event.EventGlobalMessage;
|
||||
|
||||
"error.custom": event.EventErrorCustom;
|
||||
"error.permission": event.EventErrorPermission;
|
||||
|
||||
"connection.hostname.resolved": event.EventConnectionHostnameResolved;
|
||||
"connection.hostname.resolve": event.EventConnectionHostnameResolve;
|
||||
"connection.hostname.resolve.error": event.EventConnectionHostnameResolveError;
|
||||
"connection.failed": event.EventConnectionFailed;
|
||||
"connection.login": event.EventConnectionLogin;
|
||||
"connection.connected": event.EventConnectionConnected;
|
||||
"connection.voice.dropped": event.EventConnectionVoiceDropped;
|
||||
"connection.voice.connect": event.EventConnectionVoiceConnect;
|
||||
"connection.voice.connect.failed": event.EventConnectionVoiceConnectFailed;
|
||||
"connection.voice.connect.succeeded": event.EventConnectionVoiceConnectSucceeded;
|
||||
"connection.command.error": event.EventConnectionCommandError;
|
||||
|
||||
"reconnect.scheduled": event.EventReconnectScheduled;
|
||||
"reconnect.canceled": event.EventReconnectCanceled;
|
||||
"reconnect.execute": event.EventReconnectExecute;
|
||||
|
||||
"server.welcome.message": event.EventWelcomeMessage;
|
||||
"server.host.message": event.EventWelcomeMessage;
|
||||
"server.host.message.disconnect": event.EventHostMessageDisconnect;
|
||||
|
||||
"server.closed": event.EventServerClosed;
|
||||
"server.requires.password": event.EventServerRequiresPassword;
|
||||
"server.banned": event.EventServerBanned;
|
||||
|
||||
"client.view.enter": event.EventClientEnter;
|
||||
"client.view.move": event.EventClientMove;
|
||||
"client.view.leave": event.EventClientLeave;
|
||||
|
||||
"client.view.enter.own.channel": event.EventClientEnter;
|
||||
"client.view.move.own.channel": event.EventClientMove;
|
||||
"client.view.leave.own.channel": event.EventClientLeave;
|
||||
|
||||
"client.view.move.own": event.EventClientMove;
|
||||
|
||||
"client.nickname.change.failed": event.EventClientNicknameChangeFailed,
|
||||
"client.nickname.changed": event.EventClientNicknameChanged,
|
||||
"client.nickname.changed.own": event.EventClientNicknameChanged,
|
||||
|
||||
"channel.create": event.EventChannelCreate,
|
||||
"channel.show": event.EventChannelToggle,
|
||||
"channel.hide": event.EventChannelToggle,
|
||||
"channel.delete": event.EventChannelDelete,
|
||||
|
||||
"client.poke.received": event.EventClientPokeReceived,
|
||||
"client.poke.send": event.EventClientPokeSend,
|
||||
|
||||
"private.message.received": event.EventPrivateMessageReceived,
|
||||
"private.message.send": event.EventPrivateMessageSend,
|
||||
|
||||
"webrtc.fatal.error": event.EventWebrtcFatalError
|
||||
|
||||
"disconnected": any;
|
||||
}
|
||||
|
||||
export interface EventDispatcher<EventType extends keyof TypeInfo> {
|
||||
log(data: TypeInfo[EventType], logger: ServerEventLog) : React.ReactNode;
|
||||
notify(data: TypeInfo[EventType], logger: ServerEventLog);
|
||||
sound(data: TypeInfo[EventType], logger: ServerEventLog);
|
||||
}
|
||||
|
||||
export interface ServerLogUIEvents {
|
||||
"query_log": {},
|
||||
"notify_log": {
|
||||
log: LogMessage[]
|
||||
},
|
||||
"notify_log_add": {
|
||||
event: LogMessage
|
||||
},
|
||||
"notify_show": {}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import {EventType} from "../../../ui/frames/log/Definitions";
|
||||
import {Settings, settings} from "../../../settings";
|
||||
import {settings, Settings} from "tc-shared/settings";
|
||||
import {EventType, TypeInfo} from "tc-shared/connectionlog/Definitions";
|
||||
|
||||
const focusDefaultStatus = {};
|
||||
focusDefaultStatus[EventType.CLIENT_POKE_RECEIVED] = true;
|
||||
const focusDefaultStatus: {[T in keyof TypeInfo]?: boolean} = {};
|
||||
focusDefaultStatus["client.poke.received"] = true;
|
||||
|
||||
export function requestWindowFocus() {
|
||||
if(__build.target === "web") {
|
|
@ -1,29 +1,27 @@
|
|||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import * as log from "../../../log";
|
||||
import {LogCategory} from "../../../log";
|
||||
import {EventClient, EventServerAddress, EventType, TypeInfo} from "../../../ui/frames/log/Definitions";
|
||||
import {renderBBCodeAsText} from "../../../text/bbcode";
|
||||
import {format_time} from "../../../ui/frames/chat";
|
||||
import {ViewReasonId} from "../../../ConnectionHandler";
|
||||
import {findLogDispatcher} from "../../../ui/frames/log/DispatcherLog";
|
||||
import {formatDate} from "../../../MessageFormatter";
|
||||
import {Settings, settings} from "../../../settings";
|
||||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
import {getIconManager} from "tc-shared/file/Icons";
|
||||
import { tra, tr } from "tc-shared/i18n/localize";
|
||||
import {EventClient, EventServerAddress, EventType, TypeInfo} from "tc-shared/connectionlog/Definitions";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {format_time} from "tc-shared/ui/frames/chat";
|
||||
import {ViewReasonId} from "tc-shared/ConnectionHandler";
|
||||
import {formatDate} from "tc-shared/MessageFormatter";
|
||||
import {renderBBCodeAsText} from "tc-shared/text/bbcode";
|
||||
import {LogCategory, logInfo} from "tc-shared/log";
|
||||
|
||||
export type DispatcherLog<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;
|
||||
notificationDefaultStatus[EventType.CONNECTION_VOICE_DROPPED] = true;
|
||||
const notificationDefaultStatus: {[T in keyof TypeInfo]?: boolean} = {};
|
||||
notificationDefaultStatus["client.poke.received"] = true;
|
||||
notificationDefaultStatus["server.banned"] = true;
|
||||
notificationDefaultStatus["server.closed"] = true;
|
||||
notificationDefaultStatus["server.host.message.disconnect"] = true;
|
||||
notificationDefaultStatus["global.message"] = true;
|
||||
notificationDefaultStatus["connection.failed"] = true;
|
||||
notificationDefaultStatus["private.message.received"] = true;
|
||||
notificationDefaultStatus["connection.voice.dropped"] = true;
|
||||
|
||||
let windowFocused = false;
|
||||
|
||||
|
@ -210,7 +208,7 @@ registerDispatcher(EventType.SERVER_BANNED, (data, handlerId) => {
|
|||
|
||||
spawnServerNotification(handlerId, {
|
||||
body: data.invoker.client_id > 0 ? tra("You've been banned from the server by {0} for {1}.{2}", data.invoker.client_name, time, reason) :
|
||||
tra("You've been banned from the server for {0}.{1}", time, reason)
|
||||
tra("You've been banned from the server for {0}.{1}", time, reason)
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -320,7 +318,7 @@ registerDispatcher(EventType.CLIENT_VIEW_MOVE, (data, handlerId) => {
|
|||
});
|
||||
});
|
||||
|
||||
registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL, findLogDispatcher(EventType.CLIENT_VIEW_MOVE));
|
||||
registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL, findNotificationDispatcher(EventType.CLIENT_VIEW_MOVE));
|
||||
|
||||
registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN, (data, handlerId) => {
|
||||
let message;
|
||||
|
@ -406,7 +404,7 @@ registerDispatcher(EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL, (data, handlerId) =>
|
|||
break;
|
||||
|
||||
default:
|
||||
return findLogDispatcher(EventType.CLIENT_VIEW_LEAVE)(data, handlerId, EventType.CLIENT_VIEW_LEAVE);
|
||||
return findNotificationDispatcher("client.view.leave")(data, handlerId, EventType.CLIENT_VIEW_LEAVE);
|
||||
}
|
||||
|
||||
spawnClientNotification(handlerId, data.client, {
|
||||
|
@ -482,19 +480,19 @@ registerDispatcher(EventType.PRIVATE_MESSAGE_RECEIVED, (data, handlerId) => {
|
|||
});
|
||||
|
||||
registerDispatcher(EventType.WEBRTC_FATAL_ERROR, (data, handlerId) => {
|
||||
if(data.retryTimeout) {
|
||||
let time = Math.ceil(data.retryTimeout / 1000);
|
||||
let minutes = Math.floor(time / 60);
|
||||
let seconds = time % 60;
|
||||
if(data.retryTimeout) {
|
||||
let time = Math.ceil(data.retryTimeout / 1000);
|
||||
let minutes = Math.floor(time / 60);
|
||||
let seconds = time % 60;
|
||||
|
||||
spawnServerNotification(handlerId, {
|
||||
body: tra("WebRTC connection closed due to a fatal error:\n{}\nRetry scheduled in {}.", data.message, (minutes > 0 ? minutes + "m" : "") + seconds + "s")
|
||||
});
|
||||
} else {
|
||||
spawnServerNotification(handlerId, {
|
||||
body: tra("WebRTC connection closed due to a fatal error:\n{}\nNo retry scheduled.", data.message)
|
||||
});
|
||||
}
|
||||
spawnServerNotification(handlerId, {
|
||||
body: tra("WebRTC connection closed due to a fatal error:\n{}\nRetry scheduled in {}.", data.message, (minutes > 0 ? minutes + "m" : "") + seconds + "s")
|
||||
});
|
||||
} else {
|
||||
spawnServerNotification(handlerId, {
|
||||
body: tra("WebRTC connection closed due to a fatal error:\n{}\nNo retry scheduled.", data.message)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/* snipped PRIVATE_MESSAGE_SEND */
|
||||
|
@ -509,14 +507,14 @@ loader.register_task(Stage.LOADED, {
|
|||
|
||||
/* yeahr fuck safari */
|
||||
const promise = Notification.requestPermission(result => {
|
||||
log.info(LogCategory.GENERAL, tr("Notification permission request (callback) resulted in %s"), result);
|
||||
logInfo(LogCategory.GENERAL, tr("Notification permission request (callback) resulted in %s"), result);
|
||||
})
|
||||
|
||||
if(typeof promise !== "undefined" && 'then' in promise) {
|
||||
promise.then(result => {
|
||||
log.info(LogCategory.GENERAL, tr("Notification permission request resulted in %s"), result);
|
||||
logInfo(LogCategory.GENERAL, tr("Notification permission request resulted in %s"), result);
|
||||
}).catch(error => {
|
||||
log.warn(LogCategory.GENERAL, tr("Failed to execute notification permission request: %O"), error);
|
||||
logInfo(LogCategory.GENERAL, tr("Failed to execute notification permission request: %O"), error);
|
||||
});
|
||||
}
|
||||
},
|
|
@ -0,0 +1,61 @@
|
|||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import * as React from "react";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {LogMessage, TypeInfo} from "tc-shared/connectionlog/Definitions";
|
||||
import {findNotificationDispatcher, isNotificationEnabled} from "tc-shared/connectionlog/DispatcherNotifications";
|
||||
import {isFocusRequestEnabled, requestWindowFocus} from "tc-shared/connectionlog/DispatcherFocus";
|
||||
|
||||
let uniqueLogEventId = 0;
|
||||
export interface ServerEventLogEvents {
|
||||
notify_log_add: { event: LogMessage }
|
||||
}
|
||||
|
||||
export class ServerEventLog {
|
||||
readonly events: Registry<ServerEventLogEvents>;
|
||||
private readonly connection: ConnectionHandler;
|
||||
|
||||
private maxHistoryLength: number = 100;
|
||||
private eventLog: LogMessage[] = [];
|
||||
|
||||
constructor(connection: ConnectionHandler) {
|
||||
this.connection = connection;
|
||||
this.events = new Registry<ServerEventLogEvents>();
|
||||
}
|
||||
|
||||
log<T extends keyof TypeInfo>(type: T, data: TypeInfo[T]) {
|
||||
const event = {
|
||||
data: data,
|
||||
timestamp: Date.now(),
|
||||
type: type as any,
|
||||
uniqueId: "log-" + Date.now() + "-" + (++uniqueLogEventId)
|
||||
};
|
||||
|
||||
if(settings.global(Settings.FN_EVENTS_LOG_ENABLED(type), true)) {
|
||||
this.eventLog.push(event);
|
||||
while(this.eventLog.length > this.maxHistoryLength) {
|
||||
this.eventLog.pop_front();
|
||||
}
|
||||
|
||||
this.events.fire("notify_log_add", { event: event });
|
||||
}
|
||||
|
||||
if(isNotificationEnabled(type as any)) {
|
||||
const notification = findNotificationDispatcher(type);
|
||||
if(notification) notification(data, this.connection.handlerId, type);
|
||||
}
|
||||
|
||||
if(isFocusRequestEnabled(type as any)) {
|
||||
requestWindowFocus();
|
||||
}
|
||||
}
|
||||
|
||||
getHistory() : LogMessage[] {
|
||||
return this.eventLog;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.events.destroy();
|
||||
this.eventLog = undefined;
|
||||
}
|
||||
}
|
|
@ -59,7 +59,6 @@ export abstract class AbstractChat<Events extends AbstractConversationEvents> {
|
|||
protected errorMessage: string;
|
||||
|
||||
private conversationMode: ChannelConversationMode;
|
||||
protected crossChannelChatSupported: boolean = true;
|
||||
|
||||
protected unreadTimestamp: number;
|
||||
protected unreadState: boolean = false;
|
||||
|
@ -338,6 +337,9 @@ export interface AbstractChatManagerEvents<ConversationType> {
|
|||
},
|
||||
notify_unread_count_changed: {
|
||||
unreadConversations: number
|
||||
},
|
||||
notify_cross_conversation_support_changed: {
|
||||
crossConversationSupported: boolean
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -351,18 +353,23 @@ export abstract class AbstractChatManager<ManagerEvents extends AbstractChatMana
|
|||
private selectedConversation: ConversationType;
|
||||
|
||||
private currentUnreadCount: number;
|
||||
private crossConversationSupport: boolean;
|
||||
|
||||
/* FIXME: Access modifier */
|
||||
public historyUiStates: {[id: string]: {
|
||||
executingUIHistoryQuery: boolean,
|
||||
historyErrorMessage: string | undefined,
|
||||
historyRetryTimestamp: number
|
||||
}} = {};
|
||||
executingUIHistoryQuery: boolean,
|
||||
historyErrorMessage: string | undefined,
|
||||
historyRetryTimestamp: number
|
||||
}} = {};
|
||||
|
||||
protected constructor(connection: ConnectionHandler) {
|
||||
this.connection = connection;
|
||||
this.events = new Registry<ManagerEvents>();
|
||||
this.listenerConnection = [];
|
||||
this.currentUnreadCount = 0;
|
||||
|
||||
this.crossConversationSupport = true;
|
||||
|
||||
this.listenerUnreadTimestamp = () => {
|
||||
let count = this.getConversations().filter(conversation => conversation.isUnread()).length;
|
||||
if(count === this.currentUnreadCount) { return; }
|
||||
|
@ -387,6 +394,10 @@ export abstract class AbstractChatManager<ManagerEvents extends AbstractChatMana
|
|||
return this.currentUnreadCount;
|
||||
}
|
||||
|
||||
hasCrossConversationSupport() : boolean {
|
||||
return this.crossConversationSupport;
|
||||
}
|
||||
|
||||
getSelectedConversation() : ConversationType {
|
||||
return this.selectedConversation;
|
||||
}
|
||||
|
@ -432,4 +443,13 @@ export abstract class AbstractChatManager<ManagerEvents extends AbstractChatMana
|
|||
|
||||
this.listenerUnreadTimestamp();
|
||||
}
|
||||
|
||||
protected setCrossConversationSupport(supported: boolean) {
|
||||
if(this.crossConversationSupport === supported) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.crossConversationSupport = supported;
|
||||
this.events.fire("notify_cross_conversation_support_changed", { crossConversationSupported: supported });
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
|||
import {LocalClientEntry} from "tc-shared/tree/Client";
|
||||
import {ServerCommand} from "tc-shared/connection/ConnectionBase";
|
||||
import {ChannelConversationMode} from "tc-shared/tree/Channel";
|
||||
import {ServerFeature} from "tc-shared/connection/ServerFeatures";
|
||||
|
||||
export interface ChannelConversationEvents extends AbstractConversationEvents {
|
||||
notify_messages_deleted: { messages: string[] },
|
||||
|
@ -45,7 +46,7 @@ export class ChannelConversation extends AbstractChat<ChannelConversationEvents>
|
|||
this.setUnreadTimestamp(unreadTimestamp);
|
||||
this.preventUnreadUpdate = false;
|
||||
|
||||
this.events.on(["notify_unread_state_changed", "notify_read_state_changed"], event => {
|
||||
this.events.on(["notify_unread_state_changed", "notify_read_state_changed"], () => {
|
||||
this.handle.connection.channelTree.findChannel(this.conversationId)?.setUnread(this.isReadable() && this.isUnread());
|
||||
});
|
||||
}
|
||||
|
@ -175,7 +176,6 @@ export class ChannelConversation extends AbstractChat<ChannelConversationEvents>
|
|||
break;
|
||||
|
||||
case "unsupported":
|
||||
this.crossChannelChatSupported = false;
|
||||
this.setConversationMode(ChannelConversationMode.Private, false);
|
||||
this.setCurrentMode("normal");
|
||||
break;
|
||||
|
@ -348,6 +348,16 @@ export class ChannelConversationManager extends AbstractChatManager<ChannelConve
|
|||
conversation.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
if(event.newState === ConnectionState.CONNECTED) {
|
||||
connection.serverFeatures.awaitFeatures().then(success => {
|
||||
if(!success) { return; }
|
||||
|
||||
this.setCrossConversationSupport(connection.serverFeatures.supportsFeature(ServerFeature.ADVANCED_CHANNEL_CHAT));
|
||||
});
|
||||
} else {
|
||||
this.setCrossConversationSupport(true);
|
||||
}
|
||||
}));
|
||||
|
||||
this.listenerConnection.push(connection.channelTree.events.on("notify_channel_updated", event => {
|
||||
|
|
|
@ -5,6 +5,7 @@ import {parse as parseBBCode} from "vendor/xbbcode/parser";
|
|||
import {fixupJQueryUrlTags} from "tc-shared/text/bbcode/url";
|
||||
import {fixupJQueryImageTags} from "tc-shared/text/bbcode/image";
|
||||
import "./bbcode.scss";
|
||||
import {BBCodeHandlerContext} from "vendor/xbbcode/renderer/react";
|
||||
|
||||
export const escapeBBCode = (text: string) => text.replace(/(\[)/g, "\\$1");
|
||||
|
||||
|
@ -80,11 +81,23 @@ function preprocessMessage(message: string, settings: BBCodeRenderOptions) : str
|
|||
return message;
|
||||
}
|
||||
|
||||
export const BBCodeRenderer = (props: { message: string, settings: BBCodeRenderOptions }) => (
|
||||
<XBBCodeRenderer options={{ tag_whitelist: allowedBBCodes }} renderer={rendererReact}>
|
||||
{preprocessMessage(props.message, props.settings)}
|
||||
</XBBCodeRenderer>
|
||||
);
|
||||
export const BBCodeRenderer = (props: { message: string, settings: BBCodeRenderOptions, handlerId?: string }) => {
|
||||
if(props.handlerId) {
|
||||
return (
|
||||
<BBCodeHandlerContext.Provider value={props.handlerId} key={"handler-id"}>
|
||||
<XBBCodeRenderer options={{ tag_whitelist: allowedBBCodes }} renderer={rendererReact}>
|
||||
{preprocessMessage(props.message, props.settings)}
|
||||
</XBBCodeRenderer>
|
||||
</BBCodeHandlerContext.Provider>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<XBBCodeRenderer options={{ tag_whitelist: allowedBBCodes }} renderer={rendererReact} key={"no-handler-id"}>
|
||||
{preprocessMessage(props.message, props.settings)}
|
||||
</XBBCodeRenderer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function renderBBCodeAsJQuery(message: string, settings: BBCodeRenderOptions) : JQuery[] {
|
||||
|
|
|
@ -8,4 +8,6 @@ export const rendererHTML = new HTMLRenderer(rendererReact);
|
|||
|
||||
import "./emoji";
|
||||
import "./highlight";
|
||||
import "./youtube";
|
||||
import "./youtube";
|
||||
import "./url";
|
||||
import "./image";
|
|
@ -4,8 +4,10 @@ import * as loader from "tc-loader";
|
|||
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
||||
import {TagElement} from "vendor/xbbcode/elements";
|
||||
import * as React from "react";
|
||||
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
||||
import ReactRenderer, {BBCodeHandlerContext} from "vendor/xbbcode/renderer/react";
|
||||
import {rendererReact, rendererText} from "tc-shared/text/bbcode/renderer";
|
||||
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
|
||||
import {useContext} from "react";
|
||||
|
||||
function spawnUrlContextMenu(pageX: number, pageY: number, target: string) {
|
||||
contextmenu.spawn_context_menu(pageX, pageY, {
|
||||
|
@ -31,6 +33,8 @@ function spawnUrlContextMenu(pageX: number, pageY: number, target: string) {
|
|||
});
|
||||
}
|
||||
|
||||
const ClientUrlRegex = /client:\/\/([0-9]+)\/([-A-Za-z0-9+/=]+)~/g;
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "XBBCode code tag init",
|
||||
function: async () => {
|
||||
|
@ -39,17 +43,36 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
|||
const regexUrl = /^(?:[a-zA-Z]{1,16}):(?:\/{1,3}|\\)[-a-zA-Z0-9:;,@#%&()~_?+=\/\\.]*$/g;
|
||||
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
||||
render(element: TagElement, renderer: ReactRenderer): React.ReactNode {
|
||||
let target;
|
||||
if (!element.options)
|
||||
let target: string;
|
||||
if (!element.options) {
|
||||
target = rendererText.render(element);
|
||||
else
|
||||
} else {
|
||||
target = element.options;
|
||||
}
|
||||
|
||||
regexUrl.lastIndex = 0;
|
||||
if (!regexUrl.test(target))
|
||||
if (!regexUrl.test(target)) {
|
||||
target = '#';
|
||||
}
|
||||
|
||||
const handlerId = useContext(BBCodeHandlerContext);
|
||||
|
||||
if(handlerId) {
|
||||
/* TS3-Protocol for a client */
|
||||
if(target.match(ClientUrlRegex)) {
|
||||
const clientData = target.match(ClientUrlRegex);
|
||||
const clientDatabaseId = parseInt(clientData[1]);
|
||||
const clientUniqueId = clientDatabaseId[2];
|
||||
|
||||
return <ClientTag
|
||||
clientName={rendererText.renderContent(element).join("")}
|
||||
clientUniqueId={clientUniqueId}
|
||||
clientDatabaseId={clientDatabaseId > 0 ? clientDatabaseId : undefined}
|
||||
handlerId={handlerId}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO: Implement client URLs */
|
||||
return <a key={"er-" + ++reactId} className={"xbbcode xbbcode-tag-url"} href={target} target={"_blank"} onContextMenu={event => {
|
||||
event.preventDefault();
|
||||
spawnUrlContextMenu(event.pageX, event.pageY, target);
|
||||
|
|
|
@ -4,7 +4,6 @@ import {renderMarkdownAsBBCode} from "../text/markdown";
|
|||
import {escapeBBCode} from "../text/bbcode";
|
||||
import {parse as parseBBCode} from "vendor/xbbcode/parser";
|
||||
import {TagElement} from "vendor/xbbcode/elements";
|
||||
import * as React from "react";
|
||||
import {regexImage} from "tc-shared/text/bbcode/image";
|
||||
|
||||
interface UrlKnifeUrl {
|
||||
|
|
|
@ -18,10 +18,10 @@ import {formatMessage} from "../ui/frames/chat";
|
|||
import {Registry} from "../events";
|
||||
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry";
|
||||
import {spawnFileTransferModal} from "../ui/modal/transfer/ModalFileTransfer";
|
||||
import {EventChannelData} from "../ui/frames/log/Definitions";
|
||||
import {ErrorCode} from "../connection/ErrorCode";
|
||||
import {ClientIcon} from "svg-sprites/client-icons";
|
||||
import { tr } from "tc-shared/i18n/localize";
|
||||
import {EventChannelData} from "tc-shared/connectionlog/Definitions";
|
||||
|
||||
export enum ChannelType {
|
||||
PERMANENT,
|
||||
|
@ -45,7 +45,16 @@ export enum ChannelSubscribeMode {
|
|||
export enum ChannelConversationMode {
|
||||
Public = 0,
|
||||
Private = 1,
|
||||
None = 2
|
||||
None = 2,
|
||||
}
|
||||
|
||||
export enum ChannelSidebarMode {
|
||||
Conversation = 0,
|
||||
Description = 1,
|
||||
FileTransfer = 2,
|
||||
|
||||
/* Only used within client side */
|
||||
Unknown = 0xFF
|
||||
}
|
||||
|
||||
export class ChannelProperties {
|
||||
|
@ -79,8 +88,10 @@ export class ChannelProperties {
|
|||
//Only after request
|
||||
channel_description: string = "";
|
||||
|
||||
channel_conversation_mode: ChannelConversationMode = 0;
|
||||
channel_conversation_mode: ChannelConversationMode = ChannelConversationMode.Public;
|
||||
channel_conversation_history_length: number = -1;
|
||||
|
||||
channel_sidebar_mode: ChannelSidebarMode = ChannelSidebarMode.Unknown;
|
||||
}
|
||||
|
||||
export interface ChannelEvents extends ChannelTreeEntryEvents {
|
||||
|
@ -99,7 +110,8 @@ export interface ChannelEvents extends ChannelTreeEntryEvents {
|
|||
},
|
||||
notify_collapsed_state_changed: {
|
||||
collapsed: boolean
|
||||
}
|
||||
},
|
||||
notify_description_changed: {}
|
||||
}
|
||||
|
||||
export class ParsedChannelName {
|
||||
|
@ -173,10 +185,9 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
private _destroyed = false;
|
||||
|
||||
private cachedPasswordHash: string;
|
||||
private _cached_channel_description: string = undefined;
|
||||
private _cached_channel_description_promise: Promise<string> = undefined;
|
||||
private _cached_channel_description_promise_resolve: any = undefined;
|
||||
private _cached_channel_description_promise_reject: any = undefined;
|
||||
private channelDescriptionCached: boolean;
|
||||
private channelDescriptionCallback: ((success: boolean) => void)[];
|
||||
private channelDescriptionPromise: Promise<string>;
|
||||
|
||||
private collapsed: boolean;
|
||||
private subscribed: boolean;
|
||||
|
@ -212,18 +223,20 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
|
||||
this.collapsed = this.channelTree.client.settings.server(Settings.FN_SERVER_CHANNEL_COLLAPSED(this.channelId));
|
||||
this.subscriptionMode = this.channelTree.client.settings.server(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this.channelId));
|
||||
|
||||
this.channelDescriptionCached = false;
|
||||
this.channelDescriptionCallback = [];
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._destroyed = true;
|
||||
|
||||
this.channelDescriptionCallback.forEach(callback => callback(false));
|
||||
this.channelDescriptionCallback = [];
|
||||
|
||||
this.client_list.forEach(e => this.unregisterClient(e, true));
|
||||
this.client_list = [];
|
||||
|
||||
this._cached_channel_description_promise = undefined;
|
||||
this._cached_channel_description_promise_resolve = undefined;
|
||||
this._cached_channel_description_promise_reject = undefined;
|
||||
|
||||
this.channel_previous = undefined;
|
||||
this.parent = undefined;
|
||||
this.channel_next = undefined;
|
||||
|
@ -248,18 +261,39 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
return this.parsed_channel_name.text;
|
||||
}
|
||||
|
||||
getChannelDescription() : Promise<string> {
|
||||
if(this._cached_channel_description) return new Promise<string>(resolve => resolve(this._cached_channel_description));
|
||||
if(this._cached_channel_description_promise) return this._cached_channel_description_promise;
|
||||
async getChannelDescription() : Promise<string> {
|
||||
if(this.channelDescriptionPromise) {
|
||||
return this.channelDescriptionPromise;
|
||||
}
|
||||
|
||||
this.channelTree.client.serverConnection.send_command("channelgetdescription", {cid: this.channelId}).catch(error => {
|
||||
this._cached_channel_description_promise_reject(error);
|
||||
});
|
||||
const promise = this.doGetChannelDescription();
|
||||
this.channelDescriptionPromise = promise;
|
||||
promise
|
||||
.then(() => this.channelDescriptionPromise = undefined)
|
||||
.catch(() => this.channelDescriptionPromise = undefined);
|
||||
return promise;
|
||||
}
|
||||
|
||||
return this._cached_channel_description_promise = new Promise<string>((resolve, reject) => {
|
||||
this._cached_channel_description_promise_resolve = resolve;
|
||||
this._cached_channel_description_promise_reject = reject;
|
||||
});
|
||||
private async doGetChannelDescription() {
|
||||
if(!this.channelDescriptionCached) {
|
||||
await this.channelTree.client.serverConnection.send_command("channelgetdescription", {
|
||||
cid: this.channelId
|
||||
});
|
||||
|
||||
if(!this.channelDescriptionCached) {
|
||||
/* since the channel description is a low command it will not be processed in sync */
|
||||
await new Promise((resolve, reject) => {
|
||||
this.channelDescriptionCallback.push(succeeded => {
|
||||
if(succeeded) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(tr("failed to receive description"));
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
return this.properties.channel_description;
|
||||
}
|
||||
|
||||
registerClient(client: ClientEntry) {
|
||||
|
@ -411,7 +445,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
callback: () => {
|
||||
const conversation = this.channelTree.client.getChannelConversations().findOrCreateConversation(this.getChannelId());
|
||||
this.channelTree.client.getChannelConversations().setSelectedConversation(conversation);
|
||||
this.channelTree.client.getSideBar().showChannelConversations();
|
||||
this.channelTree.client.getSideBar().showChannel();
|
||||
},
|
||||
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)
|
||||
}, {
|
||||
|
@ -575,29 +609,27 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
let key = variable.key;
|
||||
let value = variable.value;
|
||||
|
||||
if(!JSON.map_field_to(this.properties, value, variable.key)) {
|
||||
/* no update */
|
||||
continue;
|
||||
const hasUpdate = JSON.map_field_to(this.properties, value, variable.key);
|
||||
|
||||
if(key == "channel_description") {
|
||||
this.channelDescriptionCached = true;
|
||||
this.channelDescriptionCallback.forEach(callback => callback(true));
|
||||
this.channelDescriptionCallback = [];
|
||||
}
|
||||
|
||||
if(key == "channel_name") {
|
||||
this.parsed_channel_name = new ParsedChannelName(value, this.hasParent());
|
||||
} else if(key == "channel_order") {
|
||||
let order = this.channelTree.findChannel(this.properties.channel_order);
|
||||
this.channelTree.moveChannel(this, order, this.parent, false);
|
||||
} else if(key === "channel_icon_id") {
|
||||
this.properties.channel_icon_id = variable.value as any >>> 0; /* unsigned 32 bit number! */
|
||||
} else if(key == "channel_description") {
|
||||
this._cached_channel_description = undefined;
|
||||
if(this._cached_channel_description_promise_resolve)
|
||||
this._cached_channel_description_promise_resolve(value);
|
||||
this._cached_channel_description_promise = undefined;
|
||||
this._cached_channel_description_promise_resolve = undefined;
|
||||
this._cached_channel_description_promise_reject = undefined;
|
||||
} else if(key === "channel_flag_conversation_private") {
|
||||
/* "fix" for older TeaSpeak server versions (pre. 1.4.22) */
|
||||
this.properties.channel_conversation_mode = value === "1" ? 0 : 1;
|
||||
variables.push({ key: "channel_conversation_mode", value: this.properties.channel_conversation_mode + "" });
|
||||
if(hasUpdate) {
|
||||
if(key == "channel_name") {
|
||||
this.parsed_channel_name = new ParsedChannelName(value, this.hasParent());
|
||||
} else if(key == "channel_order") {
|
||||
let order = this.channelTree.findChannel(this.properties.channel_order);
|
||||
this.channelTree.moveChannel(this, order, this.parent, false);
|
||||
} else if(key === "channel_icon_id") {
|
||||
this.properties.channel_icon_id = variable.value as any >>> 0; /* unsigned 32 bit number! */
|
||||
} else if(key === "channel_flag_conversation_private") {
|
||||
/* "fix" for older TeaSpeak server versions (pre. 1.4.22) */
|
||||
this.properties.channel_conversation_mode = value === "1" ? 0 : 1;
|
||||
variables.push({ key: "channel_conversation_mode", value: this.properties.channel_conversation_mode + "" });
|
||||
}
|
||||
}
|
||||
}
|
||||
/* devel-block(log-channel-property-updates) */
|
||||
|
@ -791,4 +823,14 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
return subscribed ? ClientIcon.ChannelGreenSubscribed : ClientIcon.ChannelGreen;
|
||||
}
|
||||
}
|
||||
|
||||
handleDescriptionChanged() {
|
||||
if(!this.channelDescriptionCached) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.channelDescriptionCached = false;
|
||||
this.properties.channel_description = undefined;
|
||||
this.events.fire("notify_description_changed");
|
||||
}
|
||||
}
|
|
@ -115,6 +115,12 @@ export class ChannelTree {
|
|||
this.tagContainer = $.spawn("div").addClass("channel-tree-container");
|
||||
renderChannelTree(this, this.tagContainer[0], { popoutButton: true });
|
||||
|
||||
this.events.on("notify_channel_list_received", () => {
|
||||
if(!this.selectedEntry) {
|
||||
this.setSelectedEntry(this.client.getClient().currentChannel());
|
||||
}
|
||||
});
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
|
@ -177,13 +183,13 @@ export class ChannelTree {
|
|||
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
|
||||
const conversation = this.client.getChannelConversations().findOrCreateConversation(this.selectedEntry.channelId);
|
||||
this.client.getChannelConversations().setSelectedConversation(conversation);
|
||||
this.client.getSideBar().showChannelConversations();
|
||||
this.client.getSideBar().showChannel();
|
||||
}
|
||||
} else if(this.selectedEntry instanceof ServerEntry) {
|
||||
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
|
||||
const conversation = this.client.getChannelConversations().findOrCreateConversation(0);
|
||||
this.client.getChannelConversations().setSelectedConversation(conversation);
|
||||
this.client.getSideBar().showChannelConversations()
|
||||
this.client.getSideBar().showChannel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -559,6 +565,24 @@ export class ChannelTree {
|
|||
}, {width: 400, maxLength: 512}).open();
|
||||
}
|
||||
});
|
||||
client_menu.push({
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: ClientIcon.ChangeNickname,
|
||||
name: tr("Send private message"),
|
||||
callback: () => {
|
||||
createInputModal(tr("Send private message"), tr("Message:<br>"), text => !!text, result => {
|
||||
if (typeof(result) === "string") {
|
||||
for (const client of clients) {
|
||||
this.client.serverConnection.send_command("sendtextmessage", {
|
||||
target: client.clientId(),
|
||||
msg: result,
|
||||
targetmode: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
}, {width: 400, maxLength: 1024 * 8}).open();
|
||||
}
|
||||
});
|
||||
}
|
||||
client_menu.push({
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
|
|
|
@ -22,7 +22,6 @@ import * as hex from "../crypto/hex";
|
|||
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry";
|
||||
import {spawnClientVolumeChange, spawnMusicBotVolumeChange} from "../ui/modal/ModalChangeVolumeNew";
|
||||
import {spawnPermissionEditorModal} from "../ui/modal/permission/ModalPermissionEditor";
|
||||
import {EventClient, EventType} from "../ui/frames/log/Definitions";
|
||||
import {W2GPluginCmdHandler} from "../video-viewer/W2GPlugin";
|
||||
import {global_client_actions} from "../events/GlobalEvents";
|
||||
import {ClientIcon} from "svg-sprites/client-icons";
|
||||
|
@ -31,6 +30,7 @@ import {VoicePlayerEvents, VoicePlayerState} from "../voice/VoicePlayer";
|
|||
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
||||
import {VideoClient} from "tc-shared/connection/VideoConnection";
|
||||
import { tr } from "tc-shared/i18n/localize";
|
||||
import {EventClient} from "tc-shared/connectionlog/Definitions";
|
||||
|
||||
export enum ClientType {
|
||||
CLIENT_VOICE,
|
||||
|
@ -573,7 +573,7 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
clid: this.clientId(),
|
||||
msg: result
|
||||
}).then(() => {
|
||||
this.channelTree.client.log.log(EventType.CLIENT_POKE_SEND, {
|
||||
this.channelTree.client.log.log("client.poke.send", {
|
||||
target: this.log_data(),
|
||||
message: result
|
||||
});
|
||||
|
@ -771,7 +771,7 @@ export class ClientEntry extends ChannelTreeEntry<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(EventType.CLIENT_NICKNAME_CHANGED, {
|
||||
this.channelTree.client.log.log("client.nickname.changed", {
|
||||
client: this.log_data(),
|
||||
new_name: variable.value,
|
||||
old_name: old_value
|
||||
|
@ -996,7 +996,7 @@ export class LocalClientEntry extends ClientEntry {
|
|||
this.updateVariables({ key: "client_nickname", value: new_name }); /* change it locally */
|
||||
return this.handle.serverConnection.send_command("clientupdate", { client_nickname: new_name }).then(() => {
|
||||
settings.changeGlobal(Settings.KEY_CONNECT_USERNAME, new_name);
|
||||
this.channelTree.client.log.log(EventType.CLIENT_NICKNAME_CHANGED_OWN, {
|
||||
this.channelTree.client.log.log("client.nickname.changed.own", {
|
||||
client: this.log_data(),
|
||||
old_name: old_name,
|
||||
new_name: new_name,
|
||||
|
@ -1004,7 +1004,7 @@ export class LocalClientEntry extends ClientEntry {
|
|||
return true;
|
||||
}).catch((e: CommandResult) => {
|
||||
this.updateVariables({ key: "client_nickname", value: old_name }); /* change it back */
|
||||
this.channelTree.client.log.log(EventType.CLIENT_NICKNAME_CHANGE_FAILED, {
|
||||
this.channelTree.client.log.log("client.nickname.change.failed", {
|
||||
reason: e.extra_message
|
||||
});
|
||||
return false;
|
||||
|
|
|
@ -193,7 +193,7 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
|||
name: tr("Join server text channel"),
|
||||
callback: () => {
|
||||
this.channelTree.client.getChannelConversations().setSelectedConversation(this.channelTree.client.getChannelConversations().findOrCreateConversation(0));
|
||||
this.channelTree.client.getSideBar().showChannelConversations();
|
||||
this.channelTree.client.getSideBar().showChannel();
|
||||
},
|
||||
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)
|
||||
}, {
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as React from "react";
|
|||
import {SideBarEvents, SideBarType} from "tc-shared/ui/frames/SideBarDefinitions";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {LogCategory, logWarn} from "tc-shared/log";
|
||||
import {ChannelBarController} from "tc-shared/ui/frames/side/ChannelBarController";
|
||||
|
||||
export class SideBarController {
|
||||
private readonly uiEvents: Registry<SideBarEvents>;
|
||||
|
@ -18,8 +19,8 @@ export class SideBarController {
|
|||
|
||||
private header: SideHeaderController;
|
||||
private clientInfo: ClientInfoController;
|
||||
private channelConversations: ChannelConversationController;
|
||||
private privateConversations: PrivateConversationController;
|
||||
private channelBar: ChannelBarController;
|
||||
|
||||
constructor() {
|
||||
this.listenerConnection = [];
|
||||
|
@ -28,8 +29,8 @@ export class SideBarController {
|
|||
this.uiEvents.on("query_content", () => this.sendContent());
|
||||
this.uiEvents.on("query_content_data", event => this.sendContentData(event.content));
|
||||
|
||||
this.channelBar = new ChannelBarController();
|
||||
this.privateConversations = new PrivateConversationController();
|
||||
this.channelConversations = new ChannelConversationController();
|
||||
this.clientInfo = new ClientInfoController();
|
||||
this.header = new SideHeaderController();
|
||||
}
|
||||
|
@ -45,8 +46,8 @@ export class SideBarController {
|
|||
this.currentConnection = connection;
|
||||
this.header.setConnectionHandler(connection);
|
||||
this.clientInfo.setConnectionHandler(connection);
|
||||
this.channelConversations.setConnectionHandler(connection);
|
||||
this.privateConversations.setConnectionHandler(connection);
|
||||
this.channelBar.setConnectionHandler(connection);
|
||||
|
||||
if(connection) {
|
||||
this.listenerConnection.push(connection.getSideBar().events.on("notify_content_type_changed", () => this.sendContent()));
|
||||
|
@ -59,14 +60,14 @@ export class SideBarController {
|
|||
this.header?.destroy();
|
||||
this.header = undefined;
|
||||
|
||||
this.channelBar?.destroy();
|
||||
this.channelBar = undefined;
|
||||
|
||||
this.clientInfo?.destroy();
|
||||
this.clientInfo = undefined;
|
||||
|
||||
this.privateConversations?.destroy();
|
||||
this.privateConversations = undefined;
|
||||
|
||||
this.channelConversations?.destroy();
|
||||
this.channelConversations = undefined;
|
||||
}
|
||||
|
||||
renderInto(container: HTMLDivElement) {
|
||||
|
@ -93,17 +94,16 @@ export class SideBarController {
|
|||
});
|
||||
break;
|
||||
|
||||
case "channel-chat":
|
||||
case "channel":
|
||||
if(!this.currentConnection) {
|
||||
logWarn(LogCategory.GENERAL, tr("Received channel chat content data request without an active connection."));
|
||||
return;
|
||||
}
|
||||
|
||||
this.uiEvents.fire_react("notify_content_data", {
|
||||
content: "channel-chat",
|
||||
content: "channel",
|
||||
data: {
|
||||
events: this.channelConversations["uiEvents"],
|
||||
handlerId: this.currentConnection.handlerId
|
||||
events: this.channelBar.uiEvents,
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {PrivateConversationUIEvents} from "tc-shared/ui/frames/side/PrivateConversationDefinitions";
|
||||
import {AbstractConversationUiEvents} from "./side/AbstractConversationDefinitions";
|
||||
import {ClientInfoEvents} from "tc-shared/ui/frames/side/ClientInfoDefinitions";
|
||||
import {SideHeaderEvents} from "tc-shared/ui/frames/side/HeaderDefinitions";
|
||||
import {ChannelBarUiEvents} from "tc-shared/ui/frames/side/ChannelBarDefinitions";
|
||||
|
||||
/* TODO: Somehow outsource the event registries to IPC? */
|
||||
|
||||
export type SideBarType = "none" | "channel-chat" | "private-chat" | "client-info" | "music-manage";
|
||||
export type SideBarType = "none" | "channel" | "private-chat" | "client-info" | "music-manage";
|
||||
export interface SideBarTypeData {
|
||||
"none": {},
|
||||
"channel-chat": {
|
||||
events: Registry<AbstractConversationUiEvents>,
|
||||
handlerId: string
|
||||
"channel": {
|
||||
events: Registry<ChannelBarUiEvents>
|
||||
},
|
||||
"private-chat": {
|
||||
events: Registry<PrivateConversationUIEvents>,
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import {SideHeaderEvents, SideHeaderState} from "tc-shared/ui/frames/side/HeaderDefinitions";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import React = require("react");
|
||||
import {SideHeaderRenderer} from "tc-shared/ui/frames/side/HeaderRenderer";
|
||||
import {ConversationPanel} from "tc-shared/ui/frames/side/AbstractConversationRenderer";
|
||||
import {SideBarEvents, SideBarType, SideBarTypeData} from "tc-shared/ui/frames/SideBarDefinitions";
|
||||
import {useContext, useState} from "react";
|
||||
import {ClientInfoRenderer} from "tc-shared/ui/frames/side/ClientInfoRenderer";
|
||||
import {PrivateConversationsPanel} from "tc-shared/ui/frames/side/PrivateConversationRenderer";
|
||||
import {ChannelBarRenderer} from "tc-shared/ui/frames/side/ChannelBarRenderer";
|
||||
import {LogCategory, logWarn} from "tc-shared/log";
|
||||
import React = require("react");
|
||||
|
||||
const cssStyle = require("./SideBarRenderer.scss");
|
||||
|
||||
|
@ -23,17 +24,14 @@ function useContentData<T extends SideBarType>(type: T) : SideBarTypeData[T] {
|
|||
return contentData;
|
||||
}
|
||||
|
||||
const ContentRendererChannelConversation = () => {
|
||||
const contentData = useContentData("channel-chat");
|
||||
const ContentRendererChannel = () => {
|
||||
const contentData = useContentData("channel");
|
||||
if(!contentData) { return null; }
|
||||
|
||||
return (
|
||||
<ConversationPanel
|
||||
key={"channel-chat"}
|
||||
<ChannelBarRenderer
|
||||
key={"channel"}
|
||||
events={contentData.events}
|
||||
handlerId={contentData.handlerId}
|
||||
messagesDeletable={true}
|
||||
noFirstMessageOverlay={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -63,8 +61,8 @@ const ContentRendererClientInfo = () => {
|
|||
|
||||
const SideBarFrame = (props: { type: SideBarType }) => {
|
||||
switch (props.type) {
|
||||
case "channel-chat":
|
||||
return <ContentRendererChannelConversation key={props.type} />;
|
||||
case "channel":
|
||||
return <ContentRendererChannel key={props.type} />;
|
||||
|
||||
case "private-chat":
|
||||
return <ContentRendererPrivateConversation key={props.type} />;
|
||||
|
@ -88,7 +86,7 @@ const SideBarHeader = (props: { type: SideBarType, eventsHeader: Registry<SideHe
|
|||
headerState = { state: "none" };
|
||||
break;
|
||||
|
||||
case "channel-chat":
|
||||
case "channel":
|
||||
headerState = { state: "conversation", mode: "channel" };
|
||||
break;
|
||||
|
||||
|
@ -103,6 +101,11 @@ const SideBarHeader = (props: { type: SideBarType, eventsHeader: Registry<SideHe
|
|||
case "music-manage":
|
||||
headerState = { state: "music-bot" };
|
||||
break;
|
||||
|
||||
default:
|
||||
logWarn(LogCategory.GENERAL, tr("Side bar header with invalid type: %s"), props.type);
|
||||
headerState = { state: "none" };
|
||||
break;
|
||||
}
|
||||
|
||||
return <SideHeaderRenderer state={headerState} events={props.eventsHeader} />;
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {ServerEventLogUiEvents} from "tc-shared/ui/frames/log/Definitions";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
|
||||
export class ServerEventLogController {
|
||||
readonly events: Registry<ServerEventLogUiEvents>;
|
||||
|
||||
private currentConnection: ConnectionHandler;
|
||||
private listenerConnection: (() => void)[];
|
||||
|
||||
constructor() {
|
||||
this.events = new Registry<ServerEventLogUiEvents>();
|
||||
|
||||
this.events.on("query_handler_id", () => this.events.fire_react("notify_handler_id", { handlerId: this.currentConnection?.handlerId }));
|
||||
this.events.on("query_log", () => this.sendLogs());
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.listenerConnection?.forEach(callback => callback());
|
||||
this.listenerConnection = [];
|
||||
|
||||
this.events.destroy();
|
||||
}
|
||||
|
||||
setConnectionHandler(handler: ConnectionHandler) {
|
||||
if(this.currentConnection === handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listenerConnection?.forEach(callback => callback());
|
||||
this.listenerConnection = [];
|
||||
|
||||
this.currentConnection = handler;
|
||||
|
||||
if(this.currentConnection) {
|
||||
this.listenerConnection.push(this.currentConnection.log.events.on("notify_log_add", event => {
|
||||
this.events.fire_react("notify_log_add", { event: event.event });
|
||||
}));
|
||||
}
|
||||
|
||||
this.events.fire_react("notify_handler_id", { handlerId: handler?.handlerId });
|
||||
}
|
||||
|
||||
|
||||
private sendLogs() {
|
||||
const logs = this.currentConnection?.log.getHistory() || [];
|
||||
this.events.fire_react("notify_log", { events: logs });
|
||||
}
|
||||
}
|
|
@ -1,348 +1,16 @@
|
|||
import {PermissionInfo} from "../../../permission/PermissionManager";
|
||||
import {ViewReasonId} from "../../../ConnectionHandler";
|
||||
import * as React from "react";
|
||||
import {ServerEventLog} from "../../../ui/frames/log/ServerEventLog";
|
||||
import {LogMessage} from "tc-shared/connectionlog/Definitions";
|
||||
|
||||
/* FIXME: Remove this! */
|
||||
export enum EventType {
|
||||
CONNECTION_BEGIN = "connection.begin",
|
||||
CONNECTION_HOSTNAME_RESOLVE = "connection.hostname.resolve",
|
||||
CONNECTION_HOSTNAME_RESOLVE_ERROR = "connection.hostname.resolve.error",
|
||||
CONNECTION_HOSTNAME_RESOLVED = "connection.hostname.resolved",
|
||||
CONNECTION_LOGIN = "connection.login",
|
||||
CONNECTION_CONNECTED = "connection.connected",
|
||||
CONNECTION_FAILED = "connection.failed",
|
||||
export interface ServerEventLogUiEvents {
|
||||
query_handler_id: {},
|
||||
query_log: {},
|
||||
|
||||
DISCONNECTED = "disconnected",
|
||||
|
||||
CONNECTION_VOICE_CONNECT = "connection.voice.connect",
|
||||
CONNECTION_VOICE_CONNECT_FAILED = "connection.voice.connect.failed",
|
||||
CONNECTION_VOICE_CONNECT_SUCCEEDED = "connection.voice.connect.succeeded",
|
||||
CONNECTION_VOICE_DROPPED = "connection.voice.dropped",
|
||||
|
||||
CONNECTION_COMMAND_ERROR = "connection.command.error",
|
||||
|
||||
GLOBAL_MESSAGE = "global.message",
|
||||
|
||||
SERVER_WELCOME_MESSAGE = "server.welcome.message",
|
||||
SERVER_HOST_MESSAGE = "server.host.message",
|
||||
SERVER_HOST_MESSAGE_DISCONNECT = "server.host.message.disconnect",
|
||||
|
||||
SERVER_CLOSED = "server.closed",
|
||||
SERVER_BANNED = "server.banned",
|
||||
SERVER_REQUIRES_PASSWORD = "server.requires.password",
|
||||
|
||||
CLIENT_VIEW_ENTER = "client.view.enter",
|
||||
CLIENT_VIEW_LEAVE = "client.view.leave",
|
||||
CLIENT_VIEW_MOVE = "client.view.move",
|
||||
|
||||
CLIENT_VIEW_ENTER_OWN_CHANNEL = "client.view.enter.own.channel",
|
||||
CLIENT_VIEW_LEAVE_OWN_CHANNEL = "client.view.leave.own.channel",
|
||||
CLIENT_VIEW_MOVE_OWN_CHANNEL = "client.view.move.own.channel",
|
||||
|
||||
CLIENT_VIEW_MOVE_OWN = "client.view.move.own",
|
||||
|
||||
CLIENT_NICKNAME_CHANGED = "client.nickname.changed",
|
||||
CLIENT_NICKNAME_CHANGED_OWN = "client.nickname.changed.own",
|
||||
CLIENT_NICKNAME_CHANGE_FAILED = "client.nickname.change.failed",
|
||||
|
||||
CLIENT_SERVER_GROUP_ADD = "client.server.group.add",
|
||||
CLIENT_SERVER_GROUP_REMOVE = "client.server.group.remove",
|
||||
CLIENT_CHANNEL_GROUP_CHANGE = "client.channel.group.change",
|
||||
|
||||
PRIVATE_MESSAGE_RECEIVED = "private.message.received",
|
||||
PRIVATE_MESSAGE_SEND = "private.message.send",
|
||||
|
||||
CHANNEL_CREATE = "channel.create",
|
||||
CHANNEL_DELETE = "channel.delete",
|
||||
|
||||
ERROR_CUSTOM = "error.custom",
|
||||
ERROR_PERMISSION = "error.permission",
|
||||
|
||||
CLIENT_POKE_RECEIVED = "client.poke.received",
|
||||
CLIENT_POKE_SEND = "client.poke.send",
|
||||
|
||||
RECONNECT_SCHEDULED = "reconnect.scheduled",
|
||||
RECONNECT_EXECUTE = "reconnect.execute",
|
||||
RECONNECT_CANCELED = "reconnect.canceled",
|
||||
|
||||
WEBRTC_FATAL_ERROR = "webrtc.fatal.error"
|
||||
}
|
||||
|
||||
export type EventClient = {
|
||||
client_unique_id: string;
|
||||
client_name: string;
|
||||
client_id: number;
|
||||
}
|
||||
export type EventChannelData = {
|
||||
channel_id: number;
|
||||
channel_name: string;
|
||||
}
|
||||
export type EventServerData = {
|
||||
server_name: string;
|
||||
server_unique_id: string;
|
||||
}
|
||||
export type EventServerAddress = {
|
||||
server_hostname: string;
|
||||
server_port: number;
|
||||
}
|
||||
|
||||
export namespace event {
|
||||
export type EventGlobalMessage = {
|
||||
isOwnMessage: boolean;
|
||||
sender: EventClient;
|
||||
message: string;
|
||||
}
|
||||
export type EventConnectBegin = {
|
||||
address: EventServerAddress;
|
||||
client_nickname: string;
|
||||
}
|
||||
export type EventErrorCustom = {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type EventReconnectScheduled = {
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
export type EventReconnectCanceled = { }
|
||||
export type EventReconnectExecute = { }
|
||||
|
||||
export type EventErrorPermission = {
|
||||
permission: PermissionInfo;
|
||||
}
|
||||
|
||||
export type EventWelcomeMessage = {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type EventHostMessageDisconnect = {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type EventClientMove = {
|
||||
channel_from?: EventChannelData;
|
||||
channel_from_own: boolean;
|
||||
|
||||
channel_to?: EventChannelData;
|
||||
channel_to_own: boolean;
|
||||
|
||||
client: EventClient;
|
||||
client_own: boolean;
|
||||
|
||||
invoker?: EventClient;
|
||||
|
||||
message?: string;
|
||||
reason: ViewReasonId;
|
||||
}
|
||||
|
||||
export type EventClientEnter = {
|
||||
channel_from?: EventChannelData;
|
||||
channel_to?: EventChannelData;
|
||||
|
||||
client: EventClient;
|
||||
invoker?: EventClient;
|
||||
|
||||
message?: string;
|
||||
|
||||
reason: ViewReasonId;
|
||||
ban_time?: number;
|
||||
}
|
||||
|
||||
export type EventClientLeave = {
|
||||
channel_from?: EventChannelData;
|
||||
channel_to?: EventChannelData;
|
||||
|
||||
client: EventClient;
|
||||
invoker?: EventClient;
|
||||
|
||||
message?: string;
|
||||
|
||||
reason: ViewReasonId;
|
||||
ban_time?: number;
|
||||
}
|
||||
|
||||
export type EventChannelCreate = {
|
||||
creator: EventClient,
|
||||
channel: EventChannelData,
|
||||
ownAction: boolean
|
||||
}
|
||||
|
||||
export type EventChannelToggle = {
|
||||
channel: EventChannelData
|
||||
}
|
||||
|
||||
export type EventChannelDelete = {
|
||||
deleter: EventClient,
|
||||
channel: EventChannelData,
|
||||
ownAction: boolean
|
||||
}
|
||||
|
||||
export type EventConnectionConnected = {
|
||||
serverName: string,
|
||||
serverAddress: EventServerAddress,
|
||||
own_client: EventClient;
|
||||
}
|
||||
export type EventConnectionFailed = {
|
||||
serverAddress: EventServerAddress
|
||||
}
|
||||
export type EventConnectionLogin = {}
|
||||
export type EventConnectionHostnameResolve = {};
|
||||
export type EventConnectionHostnameResolved = {
|
||||
address: EventServerAddress;
|
||||
}
|
||||
export type EventConnectionHostnameResolveError = {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type EventConnectionVoiceConnectFailed = {
|
||||
reason: string;
|
||||
reconnect_delay: number; /* if less or equal to 0 reconnect is prohibited */
|
||||
}
|
||||
|
||||
export type EventConnectionVoiceConnectSucceeded = {}
|
||||
|
||||
export type EventConnectionVoiceConnect = {
|
||||
attemptCount: number
|
||||
}
|
||||
|
||||
export type EventConnectionVoiceDropped = {}
|
||||
|
||||
export type EventConnectionCommandError = {
|
||||
error: any;
|
||||
}
|
||||
|
||||
export type EventClientNicknameChanged = {
|
||||
client: EventClient;
|
||||
|
||||
old_name: string;
|
||||
new_name: string;
|
||||
}
|
||||
|
||||
export type EventClientNicknameChangeFailed = {
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export type EventServerClosed = {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type EventServerRequiresPassword = {}
|
||||
|
||||
export type EventServerBanned = {
|
||||
message: string;
|
||||
time: number;
|
||||
|
||||
invoker: EventClient;
|
||||
}
|
||||
|
||||
export type EventClientPokeReceived = {
|
||||
sender: EventClient,
|
||||
message: string
|
||||
}
|
||||
|
||||
export type EventClientPokeSend = {
|
||||
target: EventClient,
|
||||
message: string
|
||||
}
|
||||
|
||||
export type EventPrivateMessageSend = {
|
||||
target: EventClient,
|
||||
message: string
|
||||
}
|
||||
|
||||
export type EventPrivateMessageReceived = {
|
||||
sender: EventClient,
|
||||
message: string
|
||||
}
|
||||
|
||||
export type EventWebrtcFatalError = {
|
||||
message: string,
|
||||
retryTimeout: number | 0
|
||||
}
|
||||
}
|
||||
|
||||
export type LogMessage = {
|
||||
type: EventType;
|
||||
uniqueId: string;
|
||||
timestamp: number;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export interface TypeInfo {
|
||||
"connection.begin" : event.EventConnectBegin;
|
||||
"global.message": event.EventGlobalMessage;
|
||||
|
||||
"error.custom": event.EventErrorCustom;
|
||||
"error.permission": event.EventErrorPermission;
|
||||
|
||||
"connection.hostname.resolved": event.EventConnectionHostnameResolved;
|
||||
"connection.hostname.resolve": event.EventConnectionHostnameResolve;
|
||||
"connection.hostname.resolve.error": event.EventConnectionHostnameResolveError;
|
||||
"connection.failed": event.EventConnectionFailed;
|
||||
"connection.login": event.EventConnectionLogin;
|
||||
"connection.connected": event.EventConnectionConnected;
|
||||
"connection.voice.dropped": event.EventConnectionVoiceDropped;
|
||||
"connection.voice.connect": event.EventConnectionVoiceConnect;
|
||||
"connection.voice.connect.failed": event.EventConnectionVoiceConnectFailed;
|
||||
"connection.voice.connect.succeeded": event.EventConnectionVoiceConnectSucceeded;
|
||||
"connection.command.error": event.EventConnectionCommandError;
|
||||
|
||||
"reconnect.scheduled": event.EventReconnectScheduled;
|
||||
"reconnect.canceled": event.EventReconnectCanceled;
|
||||
"reconnect.execute": event.EventReconnectExecute;
|
||||
|
||||
"server.welcome.message": event.EventWelcomeMessage;
|
||||
"server.host.message": event.EventWelcomeMessage;
|
||||
"server.host.message.disconnect": event.EventHostMessageDisconnect;
|
||||
|
||||
"server.closed": event.EventServerClosed;
|
||||
"server.requires.password": event.EventServerRequiresPassword;
|
||||
"server.banned": event.EventServerBanned;
|
||||
|
||||
"client.view.enter": event.EventClientEnter;
|
||||
"client.view.move": event.EventClientMove;
|
||||
"client.view.leave": event.EventClientLeave;
|
||||
|
||||
"client.view.enter.own.channel": event.EventClientEnter;
|
||||
"client.view.move.own.channel": event.EventClientMove;
|
||||
"client.view.leave.own.channel": event.EventClientLeave;
|
||||
|
||||
"client.view.move.own": event.EventClientMove;
|
||||
|
||||
"client.nickname.change.failed": event.EventClientNicknameChangeFailed,
|
||||
"client.nickname.changed": event.EventClientNicknameChanged,
|
||||
"client.nickname.changed.own": event.EventClientNicknameChanged,
|
||||
|
||||
"channel.create": event.EventChannelCreate,
|
||||
"channel.show": event.EventChannelToggle,
|
||||
"channel.hide": event.EventChannelToggle,
|
||||
"channel.delete": event.EventChannelDelete,
|
||||
|
||||
"client.poke.received": event.EventClientPokeReceived,
|
||||
"client.poke.send": event.EventClientPokeSend,
|
||||
|
||||
"private.message.received": event.EventPrivateMessageReceived,
|
||||
"private.message.send": event.EventPrivateMessageSend,
|
||||
|
||||
"webrtc.fatal.error": event.EventWebrtcFatalError
|
||||
|
||||
"disconnected": any;
|
||||
}
|
||||
|
||||
export interface EventDispatcher<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": {
|
||||
notify_log_add: {
|
||||
event: LogMessage
|
||||
},
|
||||
"notify_show": {}
|
||||
notify_log: {
|
||||
events: LogMessage[]
|
||||
},
|
||||
notify_handler_id: {
|
||||
handlerId: string | undefined
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
min-height: 2em;
|
||||
min-height: 1em;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
import {LogMessage, ServerLogUIEvents} from "tc-shared/ui/frames/log/Definitions";
|
||||
import {VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {useContext, useEffect, useRef, useState} from "react";
|
||||
import * as React from "react";
|
||||
import {findLogDispatcher} from "tc-shared/ui/frames/log/DispatcherLog";
|
||||
import {findLogEventRenderer} from "./RendererEvent";
|
||||
import {LogMessage} from "tc-shared/connectionlog/Definitions";
|
||||
import {ServerEventLogUiEvents} from "tc-shared/ui/frames/log/Definitions";
|
||||
import {useDependentState} from "tc-shared/ui/react-elements/Helper";
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
|
||||
const HandlerIdContext = React.createContext<string>(undefined);
|
||||
const EventsContext = React.createContext<Registry<ServerEventLogUiEvents>>(undefined);
|
||||
|
||||
const LogFallbackDispatcher = (_unused, __unused, eventType) => (
|
||||
<div className={cssStyle.errorMessage}>
|
||||
<VariadicTranslatable text={"Missing log entry builder for {0}"}>
|
||||
|
@ -15,12 +20,14 @@ const LogFallbackDispatcher = (_unused, __unused, eventType) => (
|
|||
</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);
|
||||
const LogEntryRenderer = React.memo((props: { entry: LogMessage }) => {
|
||||
const handlerId = useContext(HandlerIdContext);
|
||||
const dispatcher = findLogEventRenderer(props.entry.type as any) || LogFallbackDispatcher;
|
||||
const rendered = dispatcher(props.entry.data, handlerId, props.entry.type);
|
||||
|
||||
if(!rendered) /* hide message */
|
||||
if(!rendered) { /* hide message */
|
||||
return null;
|
||||
}
|
||||
|
||||
const date = new Date(props.entry.timestamp);
|
||||
return (
|
||||
|
@ -35,25 +42,27 @@ const LogEntryRenderer = React.memo((props: { entry: LogMessage, handlerId: stri
|
|||
);
|
||||
});
|
||||
|
||||
export const ServerLogRenderer = (props: { events: Registry<ServerLogUIEvents>, handlerId: string }) => {
|
||||
const [ logs, setLogs ] = useState<LogMessage[] | "loading">(() => {
|
||||
props.events.fire_react("query_log");
|
||||
const ServerLogRenderer = () => {
|
||||
const handlerId = useContext(HandlerIdContext);
|
||||
const events = useContext(EventsContext);
|
||||
const [ logs, setLogs ] = useDependentState<LogMessage[] | "loading">(() => {
|
||||
events.fire_react("query_log");
|
||||
return "loading";
|
||||
});
|
||||
}, [ handlerId ]);
|
||||
|
||||
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);
|
||||
events.reactUse("notify_log", event => {
|
||||
const logs = event.events.slice(0);
|
||||
logs.splice(0, Math.max(0, logs.length - 100));
|
||||
logs.sort((a, b) => a.timestamp - b.timestamp);
|
||||
setLogs(logs);
|
||||
});
|
||||
|
||||
props.events.reactUse("notify_log_add", event => {
|
||||
events.reactUse("notify_log_add", event => {
|
||||
if(logs === "loading") {
|
||||
return;
|
||||
}
|
||||
|
@ -72,10 +81,6 @@ export const ServerLogRenderer = (props: { events: Registry<ServerLogUIEvents>,
|
|||
refContainer.current.scrollTop = scrollOffset.current === "bottom" ? refContainer.current.scrollHeight : scrollOffset.current;
|
||||
};
|
||||
|
||||
props.events.reactUse("notify_show", () => {
|
||||
requestAnimationFrame(fixScroll);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const id = requestAnimationFrame(fixScroll);
|
||||
return () => cancelAnimationFrame(id);
|
||||
|
@ -91,7 +96,23 @@ export const ServerLogRenderer = (props: { events: Registry<ServerLogUIEvents>,
|
|||
|
||||
scrollOffset.current = shouldFollow ? "bottom" : top;
|
||||
}}>
|
||||
{logs === "loading" ? null : logs.map(e => <LogEntryRenderer key={e.uniqueId} entry={e} handlerId={props.handlerId} />)}
|
||||
{logs === "loading" ? null : logs.map(e => <LogEntryRenderer key={e.uniqueId} entry={e} />)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ServerLogFrame = (props: { events: Registry<ServerEventLogUiEvents> }) => {
|
||||
const [ handlerId, setHandlerId ] = useState<string>(() => {
|
||||
props.events.fire("query_handler_id");
|
||||
return undefined;
|
||||
});
|
||||
props.events.reactUse("notify_handler_id", event => setHandlerId(event.handlerId));
|
||||
|
||||
return (
|
||||
<EventsContext.Provider value={props.events}>
|
||||
<HandlerIdContext.Provider value={handlerId}>
|
||||
<ServerLogRenderer />
|
||||
</HandlerIdContext.Provider>
|
||||
</EventsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {ViewReasonId} from "tc-shared/ConnectionHandler";
|
||||
import {EventChannelData, EventClient, EventType, TypeInfo} from "tc-shared/ui/frames/log/Definitions";
|
||||
import * as React from "react";
|
||||
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {formatDate} from "tc-shared/MessageFormatter";
|
||||
|
@ -8,22 +7,23 @@ import {format_time} from "tc-shared/ui/frames/chat";
|
|||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {XBBCodeRenderer} from "vendor/xbbcode/react";
|
||||
import {ChannelTag, ClientTag} from "tc-shared/ui/tree/EntryTags";
|
||||
import {EventChannelData, EventClient, EventType, TypeInfo} from "tc-shared/connectionlog/Definitions";
|
||||
|
||||
const cssStyle = require("./DispatcherLog.scss");
|
||||
const cssStyleRenderer = require("./Renderer.scss");
|
||||
|
||||
export type DispatcherLog<T extends keyof TypeInfo> = (data: TypeInfo[T], handlerId: string, eventType: T) => React.ReactNode;
|
||||
export type RendererEvent<T extends keyof TypeInfo> = (data: TypeInfo[T], handlerId: string, eventType: T) => React.ReactNode;
|
||||
|
||||
const dispatchers: {[key: string]: DispatcherLog<any>} = { };
|
||||
function registerDispatcher<T extends keyof TypeInfo>(key: T, builder: DispatcherLog<T>) {
|
||||
dispatchers[key] = builder;
|
||||
const dispatchers: {[T in keyof TypeInfo]?: RendererEvent<T>} = { };
|
||||
function registerRenderer<T extends keyof TypeInfo>(key: T, builder: RendererEvent<T>) {
|
||||
dispatchers[key] = builder as any;
|
||||
}
|
||||
|
||||
export function findLogDispatcher<T extends keyof TypeInfo>(type: T) : DispatcherLog<T> {
|
||||
return dispatchers[type];
|
||||
export function findLogEventRenderer<T extends keyof TypeInfo>(type: T) : RendererEvent<T> {
|
||||
return dispatchers[type] as any;
|
||||
}
|
||||
|
||||
export function getRegisteredLogDispatchers() : TypeInfo[] {
|
||||
export function getRegisteredLogEventRenderer() : TypeInfo[] {
|
||||
return Object.keys(dispatchers) as any;
|
||||
}
|
||||
|
||||
|
@ -46,66 +46,66 @@ const ChannelRenderer = (props: { channel: EventChannelData, handlerId: string,
|
|||
/>
|
||||
);
|
||||
|
||||
registerDispatcher(EventType.ERROR_CUSTOM, data => <div className={cssStyleRenderer.errorMessage}>{data.message}</div>);
|
||||
registerRenderer(EventType.ERROR_CUSTOM, data => <div className={cssStyleRenderer.errorMessage}>{data.message}</div>);
|
||||
|
||||
registerDispatcher(EventType.CONNECTION_BEGIN, data => (
|
||||
registerRenderer(EventType.CONNECTION_BEGIN, data => (
|
||||
<VariadicTranslatable text={"Connecting to {0}{1}"}>
|
||||
<>{data.address.server_hostname}</>
|
||||
<>{data.address.server_port == 9987 ? "" : (":" + data.address.server_port)}</>
|
||||
</VariadicTranslatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVE, () => (
|
||||
registerRenderer(EventType.CONNECTION_HOSTNAME_RESOLVE, () => (
|
||||
<Translatable>Resolving hostname</Translatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.CONNECTION_HOSTNAME_RESOLVED, data => (
|
||||
registerRenderer(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 => (
|
||||
registerRenderer(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, () => (
|
||||
registerRenderer(EventType.CONNECTION_LOGIN, () => (
|
||||
<Translatable>Logging in...</Translatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.CONNECTION_FAILED, () => (
|
||||
registerRenderer(EventType.CONNECTION_FAILED, () => (
|
||||
<Translatable>Connect failed.</Translatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.CONNECTION_CONNECTED, (data,handlerId) => (
|
||||
registerRenderer(EventType.CONNECTION_CONNECTED, (data,handlerId) => (
|
||||
<VariadicTranslatable text={"Connected as {0}"}>
|
||||
<ClientRenderer client={data.own_client} handlerId={handlerId} />
|
||||
</VariadicTranslatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.CONNECTION_VOICE_CONNECT, () => (
|
||||
registerRenderer(EventType.CONNECTION_VOICE_CONNECT, () => (
|
||||
<Translatable>Connecting voice bridge.</Translatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.CONNECTION_VOICE_CONNECT_SUCCEEDED, () => (
|
||||
registerRenderer(EventType.CONNECTION_VOICE_CONNECT_SUCCEEDED, () => (
|
||||
<Translatable>Voice bridge successfully connected.</Translatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.CONNECTION_VOICE_CONNECT_FAILED, (data) => (
|
||||
registerRenderer(EventType.CONNECTION_VOICE_CONNECT_FAILED, (data) => (
|
||||
<VariadicTranslatable text={"Failed to setup voice bridge: {0}. Allow reconnect: {1}"}>
|
||||
<>{data.reason}</>
|
||||
{data.reconnect_delay > 0 ? <Translatable>Yes</Translatable> : <Translatable>No</Translatable>}
|
||||
</VariadicTranslatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.CONNECTION_VOICE_DROPPED, () => (
|
||||
registerRenderer(EventType.CONNECTION_VOICE_DROPPED, () => (
|
||||
<Translatable>Voice bridge has been dropped. Trying to reconnect.</Translatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.ERROR_PERMISSION, data => (
|
||||
registerRenderer(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>}</>
|
||||
|
@ -113,7 +113,7 @@ registerDispatcher(EventType.ERROR_PERMISSION, data => (
|
|||
</div>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.CLIENT_VIEW_ENTER, (data, handlerId) => {
|
||||
registerRenderer(EventType.CLIENT_VIEW_ENTER, (data, handlerId) => {
|
||||
switch (data.reason) {
|
||||
case ViewReasonId.VREASON_USER_ACTION:
|
||||
if(data.channel_from) {
|
||||
|
@ -189,7 +189,7 @@ registerDispatcher(EventType.CLIENT_VIEW_ENTER, (data, handlerId) => {
|
|||
}
|
||||
});
|
||||
|
||||
registerDispatcher(EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL, (data, handlerId) => {
|
||||
registerRenderer(EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL, (data, handlerId) => {
|
||||
switch (data.reason) {
|
||||
case ViewReasonId.VREASON_USER_ACTION:
|
||||
if(data.channel_from) {
|
||||
|
@ -265,7 +265,7 @@ registerDispatcher(EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL, (data, handlerId) =>
|
|||
}
|
||||
});
|
||||
|
||||
registerDispatcher(EventType.CLIENT_VIEW_MOVE, (data, handlerId) => {
|
||||
registerRenderer(EventType.CLIENT_VIEW_MOVE, (data, handlerId) => {
|
||||
switch (data.reason) {
|
||||
case ViewReasonId.VREASON_MOVED:
|
||||
return (
|
||||
|
@ -308,9 +308,9 @@ registerDispatcher(EventType.CLIENT_VIEW_MOVE, (data, handlerId) => {
|
|||
}
|
||||
});
|
||||
|
||||
registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL, findLogDispatcher(EventType.CLIENT_VIEW_MOVE));
|
||||
registerRenderer(EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL, findLogEventRenderer(EventType.CLIENT_VIEW_MOVE));
|
||||
|
||||
registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN, (data, handlerId) => {
|
||||
registerRenderer(EventType.CLIENT_VIEW_MOVE_OWN, (data, handlerId) => {
|
||||
switch (data.reason) {
|
||||
case ViewReasonId.VREASON_MOVED:
|
||||
return (
|
||||
|
@ -353,7 +353,7 @@ registerDispatcher(EventType.CLIENT_VIEW_MOVE_OWN, (data, handlerId) => {
|
|||
}
|
||||
});
|
||||
|
||||
registerDispatcher(EventType.CLIENT_VIEW_LEAVE, (data, handlerId) => {
|
||||
registerRenderer(EventType.CLIENT_VIEW_LEAVE, (data, handlerId) => {
|
||||
switch (data.reason) {
|
||||
case ViewReasonId.VREASON_USER_ACTION:
|
||||
return (
|
||||
|
@ -434,7 +434,7 @@ registerDispatcher(EventType.CLIENT_VIEW_LEAVE, (data, handlerId) => {
|
|||
}
|
||||
});
|
||||
|
||||
registerDispatcher(EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL, (data, handlerId) => {
|
||||
registerRenderer(EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL, (data, handlerId) => {
|
||||
switch (data.reason) {
|
||||
case ViewReasonId.VREASON_USER_ACTION:
|
||||
return (
|
||||
|
@ -456,24 +456,24 @@ registerDispatcher(EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL, (data, handlerId) =>
|
|||
);
|
||||
|
||||
default:
|
||||
return findLogDispatcher(EventType.CLIENT_VIEW_LEAVE)(data, handlerId, EventType.CLIENT_VIEW_LEAVE);
|
||||
return findLogEventRenderer(EventType.CLIENT_VIEW_LEAVE)(data, handlerId, EventType.CLIENT_VIEW_LEAVE);
|
||||
}
|
||||
});
|
||||
|
||||
registerDispatcher(EventType.SERVER_WELCOME_MESSAGE,data => (
|
||||
<BBCodeRenderer message={"[color=green]" + data.message + "[/color]"} settings={{convertSingleUrls: false}} />
|
||||
registerRenderer(EventType.SERVER_WELCOME_MESSAGE,(data, handlerId) => (
|
||||
<BBCodeRenderer message={"[color=green]" + data.message + "[/color]"} settings={{convertSingleUrls: false}} handlerId={handlerId} />
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.SERVER_HOST_MESSAGE,data => (
|
||||
<BBCodeRenderer message={"[color=green]" + data.message + "[/color]"} settings={{convertSingleUrls: false}} />
|
||||
registerRenderer(EventType.SERVER_HOST_MESSAGE,(data, handlerId) => (
|
||||
<BBCodeRenderer message={"[color=green]" + data.message + "[/color]"} settings={{convertSingleUrls: false}} handlerId={handlerId} />
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.SERVER_HOST_MESSAGE_DISCONNECT,data => (
|
||||
<BBCodeRenderer message={"[color=red]" + data.message + "[/color]"} settings={{convertSingleUrls: false}} />
|
||||
registerRenderer(EventType.SERVER_HOST_MESSAGE_DISCONNECT,(data, handlerId) => (
|
||||
<BBCodeRenderer message={"[color=red]" + data.message + "[/color]"} settings={{convertSingleUrls: false}} handlerId={handlerId} />
|
||||
));
|
||||
|
||||
|
||||
registerDispatcher(EventType.CLIENT_NICKNAME_CHANGED,(data, handlerId) => (
|
||||
registerRenderer(EventType.CLIENT_NICKNAME_CHANGED,(data, handlerId) => (
|
||||
<VariadicTranslatable text={"{0} changed his nickname from \"{1}\" to \"{2}\""}>
|
||||
<ClientRenderer client={data.client} handlerId={handlerId} />
|
||||
<>{data.old_name}</>
|
||||
|
@ -481,44 +481,44 @@ registerDispatcher(EventType.CLIENT_NICKNAME_CHANGED,(data, handlerId) => (
|
|||
</VariadicTranslatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.CLIENT_NICKNAME_CHANGED_OWN,() => (
|
||||
registerRenderer(EventType.CLIENT_NICKNAME_CHANGED_OWN,() => (
|
||||
<Translatable>Nickname successfully changed.</Translatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.CLIENT_NICKNAME_CHANGE_FAILED,(data) => (
|
||||
registerRenderer(EventType.CLIENT_NICKNAME_CHANGE_FAILED,(data) => (
|
||||
<VariadicTranslatable text={"Failed to change nickname: {0}"}>
|
||||
<>{data.reason}</>
|
||||
</VariadicTranslatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.GLOBAL_MESSAGE, (data, handlerId) => <>
|
||||
registerRenderer(EventType.GLOBAL_MESSAGE, (data, handlerId) => <>
|
||||
<VariadicTranslatable text={"{} send a server message: {1}"}>
|
||||
<ClientRenderer client={data.sender} handlerId={handlerId} />
|
||||
<XBBCodeRenderer>{data.message}</XBBCodeRenderer>
|
||||
<BBCodeRenderer settings={{ convertSingleUrls: false }} message={data.message} handlerId={handlerId} />
|
||||
</VariadicTranslatable>
|
||||
</>);
|
||||
|
||||
|
||||
registerDispatcher(EventType.DISCONNECTED,() => (
|
||||
registerRenderer(EventType.DISCONNECTED,() => (
|
||||
<Translatable>Disconnected from server</Translatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.RECONNECT_SCHEDULED,data => (
|
||||
registerRenderer(EventType.RECONNECT_SCHEDULED,data => (
|
||||
<VariadicTranslatable text={"Reconnecting in {0}."}>
|
||||
<>{format_time(data.timeout, tr("now"))}</>
|
||||
</VariadicTranslatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.RECONNECT_CANCELED,() => (
|
||||
registerRenderer(EventType.RECONNECT_CANCELED,() => (
|
||||
<Translatable>Reconnect canceled.</Translatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.RECONNECT_CANCELED,() => (
|
||||
registerRenderer(EventType.RECONNECT_CANCELED,() => (
|
||||
<Translatable>Reconnecting...</Translatable>
|
||||
));
|
||||
|
||||
|
||||
registerDispatcher(EventType.SERVER_BANNED,(data, handlerId) => {
|
||||
registerRenderer(EventType.SERVER_BANNED,(data, handlerId) => {
|
||||
const time = data.time === 0 ? <Translatable>ever</Translatable> : <>{format_time(data.time * 1000, tr("one second"))}</>;
|
||||
const reason = data.message ? <> <Translatable>Reason:</Translatable> {data.message}</> : undefined;
|
||||
|
||||
|
@ -544,11 +544,11 @@ registerDispatcher(EventType.SERVER_BANNED,(data, handlerId) => {
|
|||
);
|
||||
});
|
||||
|
||||
registerDispatcher(EventType.SERVER_REQUIRES_PASSWORD,() => (
|
||||
registerRenderer(EventType.SERVER_REQUIRES_PASSWORD,() => (
|
||||
<Translatable>Server requires a password to connect.</Translatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.SERVER_CLOSED,data => {
|
||||
registerRenderer(EventType.SERVER_CLOSED,data => {
|
||||
if(data.message)
|
||||
return (
|
||||
<VariadicTranslatable text={"Server has been closed ({})."}>
|
||||
|
@ -558,7 +558,7 @@ registerDispatcher(EventType.SERVER_CLOSED,data => {
|
|||
return <Translatable>Server has been closed.</Translatable>;
|
||||
});
|
||||
|
||||
registerDispatcher(EventType.CONNECTION_COMMAND_ERROR,data => {
|
||||
registerRenderer(EventType.CONNECTION_COMMAND_ERROR,data => {
|
||||
let message;
|
||||
if(typeof data.error === "string")
|
||||
message = data.error;
|
||||
|
@ -576,7 +576,7 @@ registerDispatcher(EventType.CONNECTION_COMMAND_ERROR,data => {
|
|||
)
|
||||
});
|
||||
|
||||
registerDispatcher(EventType.CHANNEL_CREATE,(data, handlerId) => {
|
||||
registerRenderer(EventType.CHANNEL_CREATE,(data, handlerId) => {
|
||||
if(data.ownAction) {
|
||||
return (
|
||||
<VariadicTranslatable text={"Channel {} has been created."}>
|
||||
|
@ -593,13 +593,13 @@ registerDispatcher(EventType.CHANNEL_CREATE,(data, handlerId) => {
|
|||
}
|
||||
});
|
||||
|
||||
registerDispatcher("channel.show",(data, handlerId) => (
|
||||
registerRenderer("channel.show",(data, handlerId) => (
|
||||
<VariadicTranslatable text={"Channel {} has appeared."}>
|
||||
<ChannelRenderer channel={data.channel} handlerId={handlerId} />
|
||||
</VariadicTranslatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.CHANNEL_DELETE,(data, handlerId) => {
|
||||
registerRenderer(EventType.CHANNEL_DELETE,(data, handlerId) => {
|
||||
if(data.ownAction) {
|
||||
return (
|
||||
<VariadicTranslatable text={"Channel {} has been deleted."}>
|
||||
|
@ -616,24 +616,24 @@ registerDispatcher(EventType.CHANNEL_DELETE,(data, handlerId) => {
|
|||
}
|
||||
});
|
||||
|
||||
registerDispatcher("channel.hide",(data, handlerId) => (
|
||||
registerRenderer("channel.hide",(data, handlerId) => (
|
||||
<VariadicTranslatable text={"Channel {} has disappeared."}>
|
||||
<ChannelRenderer channel={data.channel} handlerId={handlerId} />
|
||||
</VariadicTranslatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.CLIENT_POKE_SEND,(data, handlerId) => (
|
||||
registerRenderer(EventType.CLIENT_POKE_SEND,(data, handlerId) => (
|
||||
<VariadicTranslatable text={"You poked {}."}>
|
||||
<ClientRenderer client={data.target} handlerId={handlerId} />
|
||||
</VariadicTranslatable>
|
||||
));
|
||||
|
||||
registerDispatcher(EventType.CLIENT_POKE_RECEIVED,(data, handlerId) => {
|
||||
registerRenderer(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 }} />
|
||||
<BBCodeRenderer message={data.message} settings={{ convertSingleUrls: false }} handlerId={handlerId} />
|
||||
</VariadicTranslatable>
|
||||
);
|
||||
} else {
|
||||
|
@ -645,10 +645,10 @@ registerDispatcher(EventType.CLIENT_POKE_RECEIVED,(data, handlerId) => {
|
|||
}
|
||||
});
|
||||
|
||||
registerDispatcher(EventType.PRIVATE_MESSAGE_RECEIVED, () => undefined);
|
||||
registerDispatcher(EventType.PRIVATE_MESSAGE_SEND, () => undefined);
|
||||
registerRenderer(EventType.PRIVATE_MESSAGE_RECEIVED, () => undefined);
|
||||
registerRenderer(EventType.PRIVATE_MESSAGE_SEND, () => undefined);
|
||||
|
||||
registerDispatcher(EventType.WEBRTC_FATAL_ERROR, (data) => {
|
||||
registerRenderer(EventType.WEBRTC_FATAL_ERROR, (data) => {
|
||||
if(data.retryTimeout) {
|
||||
let time = Math.ceil(data.retryTimeout / 1000);
|
||||
let minutes = Math.floor(time / 60);
|
|
@ -1,86 +0,0 @@
|
|||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import * as React from "react";
|
||||
import {LogMessage, ServerLogUIEvents, TypeInfo} from "tc-shared/ui/frames/log/Definitions";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import {ServerLogRenderer} from "tc-shared/ui/frames/log/Renderer";
|
||||
import {findNotificationDispatcher, isNotificationEnabled} from "tc-shared/ui/frames/log/DispatcherNotifications";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {isFocusRequestEnabled, requestWindowFocus} from "tc-shared/ui/frames/log/DispatcherFocus";
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
|
||||
let uniqueLogEventId = 0;
|
||||
export class ServerEventLog {
|
||||
private readonly connection: ConnectionHandler;
|
||||
private readonly uiEvents: Registry<ServerLogUIEvents>;
|
||||
private readonly listenerHandlerVisibilityChanged;
|
||||
|
||||
private htmlTag: HTMLDivElement;
|
||||
|
||||
private maxHistoryLength: number = 100;
|
||||
|
||||
private eventLog: LogMessage[] = [];
|
||||
|
||||
constructor(connection: ConnectionHandler) {
|
||||
this.connection = connection;
|
||||
this.uiEvents = new Registry<ServerLogUIEvents>();
|
||||
this.htmlTag = document.createElement("div");
|
||||
this.htmlTag.classList.add(cssStyle.htmlTag);
|
||||
|
||||
this.uiEvents.on("query_log", () => {
|
||||
this.uiEvents.fire_react("notify_log", { log: this.eventLog.slice() });
|
||||
});
|
||||
|
||||
ReactDOM.render(<ServerLogRenderer events={this.uiEvents} handlerId={this.connection.handlerId} />, this.htmlTag);
|
||||
|
||||
this.connection.events().on("notify_visibility_changed", this.listenerHandlerVisibilityChanged =event => {
|
||||
if(event.visible) {
|
||||
this.uiEvents.fire("notify_show");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
log<T extends keyof TypeInfo>(type: T, data: TypeInfo[T]) {
|
||||
const event = {
|
||||
data: data,
|
||||
timestamp: Date.now(),
|
||||
type: type as any,
|
||||
uniqueId: "log-" + Date.now() + "-" + (++uniqueLogEventId)
|
||||
};
|
||||
|
||||
if(settings.global(Settings.FN_EVENTS_LOG_ENABLED(type), true)) {
|
||||
this.eventLog.push(event);
|
||||
while(this.eventLog.length > this.maxHistoryLength)
|
||||
this.eventLog.pop_front();
|
||||
|
||||
this.uiEvents.fire_react("notify_log_add", { event: event });
|
||||
}
|
||||
|
||||
if(isNotificationEnabled(type as any)) {
|
||||
const notification = findNotificationDispatcher(type);
|
||||
if(notification) notification(data, this.connection.handlerId, type);
|
||||
}
|
||||
|
||||
if(isFocusRequestEnabled(type as any)) {
|
||||
requestWindowFocus();
|
||||
}
|
||||
}
|
||||
|
||||
getHTMLTag() {
|
||||
return this.htmlTag;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if(this.htmlTag) {
|
||||
ReactDOM.unmountComponentAtNode(this.htmlTag);
|
||||
this.htmlTag?.remove();
|
||||
this.htmlTag = undefined;
|
||||
}
|
||||
|
||||
this.connection.events().off(this.listenerHandlerVisibilityChanged);
|
||||
this.eventLog = undefined;
|
||||
|
||||
this.uiEvents.destroy();
|
||||
}
|
||||
}
|
|
@ -29,8 +29,6 @@ export abstract class AbstractConversationController<
|
|||
protected currentSelectedConversation: ConversationType;
|
||||
protected currentSelectedListener: (() => void)[];
|
||||
|
||||
protected crossChannelChatSupported = true;
|
||||
|
||||
protected constructor() {
|
||||
this.uiEvents = new Registry<Events>();
|
||||
this.currentSelectedListener = [];
|
||||
|
@ -68,6 +66,12 @@ export abstract class AbstractConversationController<
|
|||
|
||||
protected registerConversationManagerEvents(manager: Manager) {
|
||||
this.listenerManager.push(manager.events.on("notify_selected_changed", event => this.setCurrentlySelected(event.newConversation)));
|
||||
this.listenerManager.push(manager.events.on("notify_cross_conversation_support_changed", () => {
|
||||
const currentConversation = this.getCurrentConversation();
|
||||
if(currentConversation) {
|
||||
this.reportStateToUI(currentConversation);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
protected registerConversationEvents(conversation: ConversationType) {
|
||||
|
@ -138,7 +142,7 @@ export abstract class AbstractConversationController<
|
|||
this.uiEvents.fire_react("notify_conversation_state", {
|
||||
chatId: conversation.getChatId(),
|
||||
state: "private",
|
||||
crossChannelChatSupported: this.crossChannelChatSupported
|
||||
crossChannelChatSupported: this.conversationManager.hasCrossConversationSupport()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -154,7 +158,7 @@ export abstract class AbstractConversationController<
|
|||
chatFrameMaxMessageCount: kMaxChatFrameMessageSize,
|
||||
unreadTimestamp: conversation.getUnreadTimestamp(),
|
||||
|
||||
showUserSwitchEvents: conversation.isPrivate() || !this.crossChannelChatSupported,
|
||||
showUserSwitchEvents: conversation.isPrivate() || !this.conversationManager.hasCrossConversationSupport(),
|
||||
sendEnabled: conversation.isSendEnabled(),
|
||||
|
||||
events: [...conversation.getPresentEvents(), ...conversation.getPresentMessages()]
|
||||
|
@ -251,18 +255,6 @@ export abstract class AbstractConversationController<
|
|||
return this.currentSelectedConversation;
|
||||
}
|
||||
|
||||
protected setCrossChannelChatSupport(flag: boolean) {
|
||||
if(this.crossChannelChatSupported === flag) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.crossChannelChatSupported = flag;
|
||||
const currentConversation = this.getCurrentConversation();
|
||||
if(currentConversation) {
|
||||
this.reportStateToUI(currentConversation);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler<AbstractConversationUiEvents>("query_conversation_state")
|
||||
protected handleQueryConversationState(event: AbstractConversationUiEvents["query_conversation_state"]) {
|
||||
const conversation = this.conversationManager?.findConversationById(event.chatId);
|
||||
|
|
|
@ -56,6 +56,9 @@ html:root {
|
|||
|
||||
position: relative;
|
||||
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
|
||||
.containerMessages {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
|
|
@ -26,9 +26,9 @@ import {ChatBox} from "tc-shared/ui/react-elements/ChatBox";
|
|||
|
||||
const cssStyle = require("./AbstractConversationRenderer.scss");
|
||||
|
||||
const ChatMessageTextRenderer = React.memo((props: { text: string }) => {
|
||||
const ChatMessageTextRenderer = React.memo((props: { text: string, handlerId: string }) => {
|
||||
if(typeof props.text !== "string") { debugger; }
|
||||
return <BBCodeRenderer settings={{ convertSingleUrls: true }} message={props.text || ""} />;
|
||||
return <BBCodeRenderer settings={{ convertSingleUrls: true }} message={props.text || ""} handlerId={props.handlerId} />;
|
||||
});
|
||||
|
||||
const ChatEventMessageRenderer = React.memo((props: {
|
||||
|
@ -71,7 +71,7 @@ const ChatEventMessageRenderer = React.memo((props: {
|
|||
<br /> { /* Only for copy purposes */ }
|
||||
</div>
|
||||
<div className={cssStyle.text}>
|
||||
<ChatMessageTextRenderer text={props.message.message} />
|
||||
<ChatMessageTextRenderer text={props.message.message} handlerId={props.handlerId} />
|
||||
</div>
|
||||
<br style={{ content: " ", display: "none" }} /> { /* Only for copy purposes */ }
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {ChannelBarMode, ChannelBarUiEvents} from "tc-shared/ui/frames/side/ChannelBarDefinitions";
|
||||
import {ChannelEntry, ChannelSidebarMode} from "tc-shared/tree/Channel";
|
||||
import {ChannelConversationController} from "tc-shared/ui/frames/side/ChannelConversationController";
|
||||
import {ChannelDescriptionController} from "tc-shared/ui/frames/side/ChannelDescriptionController";
|
||||
import {LocalClientEntry} from "tc-shared/tree/Client";
|
||||
|
||||
export class ChannelBarController {
|
||||
readonly uiEvents: Registry<ChannelBarUiEvents>;
|
||||
|
||||
private channelConversations: ChannelConversationController;
|
||||
private description: ChannelDescriptionController;
|
||||
|
||||
private currentConnection: ConnectionHandler;
|
||||
private listenerConnection: (() => void)[];
|
||||
|
||||
private currentChannel: ChannelEntry;
|
||||
private listenerChannel: (() => void)[];
|
||||
|
||||
constructor() {
|
||||
this.uiEvents = new Registry<ChannelBarUiEvents>();
|
||||
this.listenerConnection = [];
|
||||
this.listenerChannel = [];
|
||||
|
||||
this.channelConversations = new ChannelConversationController();
|
||||
this.description = new ChannelDescriptionController();
|
||||
|
||||
this.uiEvents.on("query_mode", () => this.notifyChannelMode());
|
||||
this.uiEvents.on("query_channel_id", () => this.notifyChannelId());
|
||||
this.uiEvents.on("query_data", event => this.notifyModeData(event.mode));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.listenerConnection.forEach(callback => callback());
|
||||
this.listenerConnection = [];
|
||||
|
||||
this.listenerChannel.forEach(callback => callback());
|
||||
this.listenerChannel = [];
|
||||
|
||||
this.currentChannel = undefined;
|
||||
this.currentConnection = undefined;
|
||||
|
||||
this.channelConversations?.destroy();
|
||||
this.channelConversations = undefined;
|
||||
|
||||
this.description?.destroy();
|
||||
this.description = undefined;
|
||||
|
||||
this.uiEvents.destroy();
|
||||
}
|
||||
|
||||
setConnectionHandler(handler: ConnectionHandler) {
|
||||
if(this.currentConnection === handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.channelConversations.setConnectionHandler(handler);
|
||||
|
||||
this.listenerConnection.forEach(callback => callback());
|
||||
this.listenerConnection = [];
|
||||
|
||||
this.currentConnection = handler;
|
||||
|
||||
const selectedEntry = handler?.channelTree.getSelectedEntry();
|
||||
if(selectedEntry instanceof ChannelEntry) {
|
||||
this.setChannel(selectedEntry);
|
||||
} else {
|
||||
this.setChannel(undefined);
|
||||
}
|
||||
|
||||
if(handler) {
|
||||
this.listenerConnection.push(handler.channelTree.events.on("notify_selected_entry_changed", event => {
|
||||
if(event.newEntry instanceof ChannelEntry) {
|
||||
this.setChannel(event.newEntry);
|
||||
}
|
||||
}));
|
||||
|
||||
this.listenerConnection.push(handler.channelTree.events.on("notify_client_moved", event => {
|
||||
if(event.client instanceof LocalClientEntry) {
|
||||
if(event.oldChannel === this.currentChannel || event.newChannel === this.currentChannel) {
|
||||
/* The mode may changed since we can now write in the channel */
|
||||
this.notifyChannelMode();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.listenerConnection.push(handler.getChannelConversations().events.on("notify_cross_conversation_support_changed", () => {
|
||||
this.notifyChannelMode();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private setChannel(channel: ChannelEntry) {
|
||||
if(this.currentChannel === channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.description.setChannel(channel);
|
||||
|
||||
this.listenerChannel.forEach(callback => callback());
|
||||
this.listenerChannel = [];
|
||||
|
||||
this.currentChannel = channel;
|
||||
this.notifyChannelId();
|
||||
|
||||
if(channel) {
|
||||
this.listenerChannel.push(channel.events.on("notify_properties_updated", event => {
|
||||
if("channel_sidebar_mode" in event.updated_properties) {
|
||||
this.notifyChannelMode();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private notifyChannelId() {
|
||||
this.uiEvents.fire_react("notify_channel_id", {
|
||||
channelId: this.currentChannel ? this.currentChannel.channelId : -1,
|
||||
handlerId: this.currentConnection ? this.currentConnection.handlerId : "unbound"
|
||||
});
|
||||
}
|
||||
|
||||
private notifyChannelMode() {
|
||||
let mode: ChannelBarMode = "none";
|
||||
|
||||
if(this.currentChannel) {
|
||||
switch(this.currentChannel.properties.channel_sidebar_mode) {
|
||||
case ChannelSidebarMode.Description:
|
||||
mode = "description";
|
||||
break;
|
||||
|
||||
case ChannelSidebarMode.FileTransfer:
|
||||
mode = "file-transfer";
|
||||
break;
|
||||
|
||||
case ChannelSidebarMode.Conversation:
|
||||
mode = "conversation";
|
||||
break;
|
||||
|
||||
case ChannelSidebarMode.Unknown:
|
||||
default:
|
||||
if(this.currentConnection) {
|
||||
const channelConversation = this.currentConnection.getChannelConversations();
|
||||
if(channelConversation.hasCrossConversationSupport() || this.currentChannel === this.currentConnection.getClient().currentChannel()) {
|
||||
mode = "conversation";
|
||||
} else {
|
||||
/* A really old TeaSpeak server or a TeamSpeak server. */
|
||||
mode = "description";
|
||||
}
|
||||
} else {
|
||||
mode = "none";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.uiEvents.fire_react("notify_mode", { mode: mode });
|
||||
}
|
||||
|
||||
private notifyModeData(mode: ChannelBarMode) {
|
||||
switch (mode) {
|
||||
case "none":
|
||||
this.uiEvents.fire_react("notify_data", { content: "none", data: {} });
|
||||
break;
|
||||
|
||||
case "conversation":
|
||||
this.uiEvents.fire_react("notify_data", {
|
||||
content: "conversation",
|
||||
data: {
|
||||
events: this.channelConversations.getUiEvents()
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case "description":
|
||||
this.uiEvents.fire_react("notify_data", {
|
||||
content: "description",
|
||||
data: {
|
||||
events: this.description.uiEvents
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case "file-transfer":
|
||||
this.uiEvents.fire_react("notify_data", {
|
||||
content: "file-transfer",
|
||||
data: {
|
||||
}
|
||||
});
|
||||
/* TODO! */
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions";
|
||||
import {ChannelDescriptionUiEvents} from "tc-shared/ui/frames/side/ChannelDescriptionDefinitions";
|
||||
|
||||
export type ChannelBarMode = "conversation" | "description" | "file-transfer" | "none";
|
||||
|
||||
export interface ChannelBarModeData {
|
||||
"conversation": {
|
||||
events: Registry<ChannelConversationUiEvents>,
|
||||
},
|
||||
"description": {
|
||||
events: Registry<ChannelDescriptionUiEvents>
|
||||
},
|
||||
"file-transfer": {
|
||||
/* TODO! */
|
||||
},
|
||||
"none": {}
|
||||
}
|
||||
|
||||
export type ChannelBarNotifyModeData<T extends keyof ChannelBarModeData> = {
|
||||
content: T,
|
||||
data: ChannelBarModeData[T]
|
||||
}
|
||||
|
||||
export interface ChannelBarUiEvents {
|
||||
query_mode: {},
|
||||
query_channel_id: {},
|
||||
query_data: { mode: ChannelBarMode },
|
||||
|
||||
notify_mode: { mode: ChannelBarMode },
|
||||
notify_channel_id: {
|
||||
channelId: number,
|
||||
handlerId: string
|
||||
},
|
||||
notify_data: ChannelBarNotifyModeData<ChannelBarMode>
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {ChannelBarMode, ChannelBarModeData, ChannelBarUiEvents} from "tc-shared/ui/frames/side/ChannelBarDefinitions";
|
||||
import {useContext, useState} from "react";
|
||||
import * as React from "react";
|
||||
import {ConversationPanel} from "tc-shared/ui/frames/side/AbstractConversationRenderer";
|
||||
import {useDependentState} from "tc-shared/ui/react-elements/Helper";
|
||||
import {ChannelDescriptionRenderer} from "tc-shared/ui/frames/side/ChannelDescriptionRenderer";
|
||||
|
||||
const EventContext = React.createContext<Registry<ChannelBarUiEvents>>(undefined);
|
||||
const ChannelContext = React.createContext<{ channelId: number, handlerId: string }>(undefined);
|
||||
|
||||
function useModeData<T extends ChannelBarMode>(type: T, dependencies: any[]) : ChannelBarModeData[T] {
|
||||
const events = useContext(EventContext);
|
||||
const [ contentData, setContentData ] = useDependentState(() => {
|
||||
events.fire("query_data", { mode: type });
|
||||
return undefined;
|
||||
}, dependencies);
|
||||
events.reactUse("notify_data", event => event.content === type && setContentData(event.data));
|
||||
|
||||
return contentData;
|
||||
}
|
||||
|
||||
const ModeRenderer = () => {
|
||||
const events = useContext(EventContext);
|
||||
const channelContext = useContext(ChannelContext);
|
||||
const [ mode, setMode ] = useDependentState<ChannelBarMode>(() => {
|
||||
events.fire("query_mode");
|
||||
return "none";
|
||||
}, [ channelContext.channelId, channelContext.handlerId ]);
|
||||
events.reactUse("notify_mode", event => setMode(event.mode));
|
||||
|
||||
switch (mode) {
|
||||
case "conversation":
|
||||
return <ModeRendererConversation key={"conversation"} />;
|
||||
|
||||
case "description":
|
||||
return <ModeRendererDescription key={"description"} />;
|
||||
|
||||
case "file-transfer":
|
||||
/* TODO! */
|
||||
return null;
|
||||
|
||||
case "none":
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const ModeRendererConversation = React.memo(() => {
|
||||
const channelContext = useContext(ChannelContext);
|
||||
const data = useModeData("conversation", [ channelContext ]);
|
||||
if(!data) { return null; }
|
||||
|
||||
return (
|
||||
<ConversationPanel
|
||||
key={"conversation"}
|
||||
events={data.events}
|
||||
handlerId={channelContext.handlerId}
|
||||
messagesDeletable={true}
|
||||
noFirstMessageOverlay={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const ModeRendererDescription = React.memo(() => {
|
||||
const channelContext = useContext(ChannelContext);
|
||||
const data = useModeData("description", [ channelContext ]);
|
||||
if(!data) { return null; }
|
||||
|
||||
return (
|
||||
<ChannelDescriptionRenderer events={data.events} />
|
||||
);
|
||||
});
|
||||
|
||||
export const ChannelBarRenderer = (props: { events: Registry<ChannelBarUiEvents> }) => {
|
||||
const [ channelContext, setChannelContext ] = useState<{ channelId: number, handlerId: string }>(() => {
|
||||
props.events.fire("query_channel_id");
|
||||
return { channelId: -1, handlerId: "unbound" };
|
||||
});
|
||||
props.events.reactUse("notify_channel_id", event => setChannelContext({ handlerId: event.handlerId, channelId: event.channelId }));
|
||||
|
||||
return (
|
||||
<EventContext.Provider value={props.events}>
|
||||
<ChannelContext.Provider value={channelContext}>
|
||||
<ModeRenderer />
|
||||
</ChannelContext.Provider>
|
||||
</EventContext.Provider>
|
||||
);
|
||||
}
|
|
@ -11,7 +11,6 @@ import {
|
|||
ChannelConversationManager,
|
||||
ChannelConversationManagerEvents
|
||||
} from "tc-shared/conversations/ChannelConversationManager";
|
||||
import {ServerFeature} from "tc-shared/connection/ServerFeatures";
|
||||
import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions";
|
||||
|
||||
export class ChannelConversationController extends AbstractConversationController<
|
||||
|
@ -75,18 +74,6 @@ export class ChannelConversationController extends AbstractConversationControlle
|
|||
|
||||
this.handlePanelShow();
|
||||
}));
|
||||
|
||||
this.connectionListener.push(connection.events().on("notify_connection_state_changed", event => {
|
||||
if(event.newState === ConnectionState.CONNECTED) {
|
||||
connection.serverFeatures.awaitFeatures().then(success => {
|
||||
if(!success) { return; }
|
||||
|
||||
this.setCrossChannelChatSupport(connection.serverFeatures.supportsFeature(ServerFeature.ADVANCED_CHANNEL_CHAT));
|
||||
});
|
||||
} else {
|
||||
this.setCrossChannelChatSupport(true);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@EventHandler<AbstractConversationUiEvents>("action_delete_message")
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
import {ChannelEntry} from "tc-shared/tree/Channel";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {
|
||||
ChannelDescriptionStatus,
|
||||
ChannelDescriptionUiEvents
|
||||
} from "tc-shared/ui/frames/side/ChannelDescriptionDefinitions";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {LogCategory, logError} from "tc-shared/log";
|
||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||
|
||||
export class ChannelDescriptionController {
|
||||
readonly uiEvents: Registry<ChannelDescriptionUiEvents>;
|
||||
private currentChannel: ChannelEntry;
|
||||
private listenerChannel: (() => void)[];
|
||||
|
||||
private descriptionSendPending = false;
|
||||
private cachedDescriptionStatus: ChannelDescriptionStatus;
|
||||
private cachedDescriptionAge: number;
|
||||
|
||||
constructor() {
|
||||
this.uiEvents = new Registry<ChannelDescriptionUiEvents>();
|
||||
this.listenerChannel = [];
|
||||
|
||||
this.uiEvents.on("query_description", () => this.notifyDescription());
|
||||
this.uiEvents.enableDebug("channel-description");
|
||||
|
||||
this.cachedDescriptionAge = 0;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.listenerChannel.forEach(callback => callback());
|
||||
this.listenerChannel = [];
|
||||
|
||||
this.currentChannel = undefined;
|
||||
|
||||
this.uiEvents.destroy();
|
||||
}
|
||||
|
||||
setChannel(channel: ChannelEntry) {
|
||||
if(this.currentChannel === channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listenerChannel.forEach(callback => callback());
|
||||
this.listenerChannel = [];
|
||||
|
||||
this.currentChannel = channel;
|
||||
this.cachedDescriptionStatus = undefined;
|
||||
|
||||
if(channel) {
|
||||
this.listenerChannel.push(channel.events.on("notify_properties_updated", event => {
|
||||
if("channel_description" in event.updated_properties) {
|
||||
this.notifyDescription().then(undefined);
|
||||
}
|
||||
}));
|
||||
|
||||
this.listenerChannel.push(channel.events.on("notify_description_changed", () => {
|
||||
this.notifyDescription().then(undefined);
|
||||
}));
|
||||
}
|
||||
|
||||
this.notifyDescription().then(undefined);
|
||||
}
|
||||
|
||||
private async notifyDescription() {
|
||||
if(this.descriptionSendPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.descriptionSendPending = true;
|
||||
try {
|
||||
if(Date.now() - this.cachedDescriptionAge > 5000 || !this.cachedDescriptionStatus) {
|
||||
await this.updateCachedDescriptionStatus();
|
||||
}
|
||||
|
||||
this.uiEvents.fire_react("notify_description", { status: this.cachedDescriptionStatus });
|
||||
} finally {
|
||||
this.descriptionSendPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async updateCachedDescriptionStatus() {
|
||||
try {
|
||||
let description;
|
||||
if(this.currentChannel) {
|
||||
description = await new Promise<any>((resolve, reject) => {
|
||||
this.currentChannel.getChannelDescription().then(resolve).catch(reject);
|
||||
setTimeout(() => reject(tr("timeout")), 5000);
|
||||
});
|
||||
}
|
||||
|
||||
this.cachedDescriptionStatus = {
|
||||
status: "success",
|
||||
description: description,
|
||||
handlerId: this.currentChannel.channelTree.client.handlerId
|
||||
};
|
||||
} catch (error) {
|
||||
if(error instanceof CommandResult) {
|
||||
if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
||||
const permission = this.currentChannel?.channelTree.client.permissions.resolveInfo(parseInt(error.json["failed_permid"]));
|
||||
this.cachedDescriptionStatus = {
|
||||
status: "no-permissions",
|
||||
failedPermission: permission ? permission.name : "unknown"
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
error = error.formattedMessage();
|
||||
} else if(typeof error !== "string") {
|
||||
logError(LogCategory.GENERAL, tr("Failed to get channel descriptions: %o"), error);
|
||||
error = tr("lookup the console");
|
||||
}
|
||||
|
||||
this.cachedDescriptionStatus = {
|
||||
status: "error",
|
||||
reason: error
|
||||
};
|
||||
}
|
||||
this.cachedDescriptionAge = Date.now();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
export type ChannelDescriptionStatus = {
|
||||
status: "success",
|
||||
description: string,
|
||||
handlerId: string
|
||||
} | {
|
||||
status: "error",
|
||||
reason: string
|
||||
} | {
|
||||
status: "no-permissions",
|
||||
failedPermission: string
|
||||
};
|
||||
|
||||
export interface ChannelDescriptionUiEvents {
|
||||
query_description: {},
|
||||
notify_description: {
|
||||
status: ChannelDescriptionStatus,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
@import "../../../../css/static/mixin";
|
||||
@import "../../../../css/static/properties";
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
color: #999;
|
||||
|
||||
&.centeredText {
|
||||
justify-content: center;
|
||||
color: var(--chat-overlay);
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: #ac5353;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.centeredText .text {
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.descriptionContainer {
|
||||
padding: .5em;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
color: #999;
|
||||
|
||||
overflow: auto;
|
||||
@include chat-scrollbar();
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import {
|
||||
ChannelDescriptionStatus,
|
||||
ChannelDescriptionUiEvents
|
||||
} from "tc-shared/ui/frames/side/ChannelDescriptionDefinitions";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import * as React from "react";
|
||||
import {useState} from "react";
|
||||
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import {allowedBBCodes, BBCodeRenderer} from "tc-shared/text/bbcode";
|
||||
import {parse as parseBBCode} from "vendor/xbbcode/parser";
|
||||
|
||||
const cssStyle = require("./ChannelDescriptionRenderer.scss");
|
||||
|
||||
const CenteredTextRenderer = (props: { children?: React.ReactElement | (React.ReactElement | string)[], className?: string }) => {
|
||||
return (
|
||||
<div className={cssStyle.container + " " + cssStyle.centeredText + " " + props.className}>
|
||||
<div className={cssStyle.text}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DescriptionRenderer = React.memo((props: { description: string, handlerId: string }) => {
|
||||
if(!props.description) {
|
||||
return (
|
||||
<CenteredTextRenderer key={"no-description"}>
|
||||
<Translatable>Channel has no description</Translatable>
|
||||
</CenteredTextRenderer>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={"description"} className={cssStyle.descriptionContainer}>
|
||||
<BBCodeRenderer settings={{ convertSingleUrls: false }} message={props.description} handlerId={props.handlerId} />
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
const DescriptionErrorRenderer = React.memo((props: { error: string }) => (
|
||||
<CenteredTextRenderer className={cssStyle.error}>
|
||||
<Translatable>An error happened while fetching the channel description:</Translatable><br />
|
||||
{props.error}
|
||||
</CenteredTextRenderer>
|
||||
));
|
||||
|
||||
const PermissionErrorRenderer = React.memo((props: { failedPermission: string }) => (
|
||||
<CenteredTextRenderer>
|
||||
<Translatable>You don't have the permission to watch the channel description.</Translatable>
|
||||
<VariadicTranslatable text={"(Missing permission {})"}><code>{props.failedPermission}</code></VariadicTranslatable>
|
||||
</CenteredTextRenderer>
|
||||
));
|
||||
|
||||
export const ChannelDescriptionRenderer = React.memo((props: { events: Registry<ChannelDescriptionUiEvents> }) => {
|
||||
const [ description, setDescription ] = useState<ChannelDescriptionStatus | { status: "loading" }>(() => {
|
||||
props.events.fire("query_description");
|
||||
return { status: "loading" };
|
||||
});
|
||||
props.events.reactUse("notify_description", event => setDescription(event.status));
|
||||
|
||||
switch (description.status) {
|
||||
case "success":
|
||||
return (
|
||||
<DescriptionRenderer description={description.description} key={"description"} handlerId={description.handlerId} />
|
||||
);
|
||||
|
||||
case "error":
|
||||
return (
|
||||
<DescriptionErrorRenderer error={description.reason} key={"error"} />
|
||||
);
|
||||
|
||||
case "no-permissions":
|
||||
return (
|
||||
<PermissionErrorRenderer failedPermission={description.failedPermission} />
|
||||
);
|
||||
|
||||
case "loading":
|
||||
default:
|
||||
return (
|
||||
<CenteredTextRenderer key={"loading"}>
|
||||
<Translatable>loading channel description</Translatable> <LoadingDots />
|
||||
</CenteredTextRenderer>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -51,7 +51,7 @@ export class SideHeaderController {
|
|||
});
|
||||
|
||||
this.uiEvents.on("action_switch_channel_chat", () => {
|
||||
this.connection.getSideBar().showChannelConversations();
|
||||
this.connection.getSideBar().showChannel();
|
||||
});
|
||||
|
||||
this.uiEvents.on("action_bot_manage", () => {
|
||||
|
|
|
@ -3,15 +3,15 @@ import {useRef, useState} from "react";
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {FlatInputField} from "tc-shared/ui/react-elements/InputField";
|
||||
import {EventType} from "tc-shared/ui/frames/log/Definitions";
|
||||
import {
|
||||
getRegisteredNotificationDispatchers,
|
||||
isNotificationEnabled
|
||||
} from "tc-shared/ui/frames/log/DispatcherNotifications";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
||||
import {Tooltip} from "tc-shared/ui/react-elements/Tooltip";
|
||||
import {isFocusRequestEnabled} from "tc-shared/ui/frames/log/DispatcherFocus";
|
||||
import {TypeInfo} from "tc-shared/connectionlog/Definitions";
|
||||
import {
|
||||
getRegisteredNotificationDispatchers,
|
||||
isNotificationEnabled
|
||||
} from "tc-shared/connectionlog/DispatcherNotifications";
|
||||
import {isFocusRequestEnabled} from "tc-shared/connectionlog/DispatcherFocus";
|
||||
|
||||
const cssStyle = require("./Notifications.scss");
|
||||
|
||||
|
@ -21,7 +21,7 @@ interface EventGroup {
|
|||
key: string;
|
||||
name: string;
|
||||
|
||||
events?: string[];
|
||||
events?: (keyof TypeInfo)[];
|
||||
subgroups?: EventGroup[];
|
||||
}
|
||||
|
||||
|
@ -340,23 +340,23 @@ const knownEventGroups: EventGroup[] = [
|
|||
key: "client-messages",
|
||||
name: "Messages",
|
||||
events: [
|
||||
EventType.CLIENT_POKE_RECEIVED,
|
||||
EventType.CLIENT_POKE_SEND,
|
||||
EventType.PRIVATE_MESSAGE_SEND,
|
||||
EventType.PRIVATE_MESSAGE_RECEIVED
|
||||
"client.poke.received",
|
||||
"client.poke.send",
|
||||
"private.message.send",
|
||||
"private.message.received"
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "client-view",
|
||||
name: "View",
|
||||
events: [
|
||||
EventType.CLIENT_VIEW_ENTER,
|
||||
EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL,
|
||||
EventType.CLIENT_VIEW_MOVE,
|
||||
EventType.CLIENT_VIEW_MOVE_OWN,
|
||||
EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL,
|
||||
EventType.CLIENT_VIEW_LEAVE,
|
||||
EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL
|
||||
"client.view.enter",
|
||||
"client.view.enter.own.channel",
|
||||
"client.view.move",
|
||||
"client.view.move.own",
|
||||
"client.view.move.own.channel",
|
||||
"client.view.leave",
|
||||
"client.view.leave.own.channel"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -365,45 +365,45 @@ const knownEventGroups: EventGroup[] = [
|
|||
key: "server",
|
||||
name: "Server",
|
||||
events: [
|
||||
EventType.GLOBAL_MESSAGE,
|
||||
EventType.SERVER_CLOSED,
|
||||
EventType.SERVER_BANNED,
|
||||
"global.message",
|
||||
"server.closed",
|
||||
"server.banned",
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "connection",
|
||||
name: "Connection",
|
||||
events: [
|
||||
EventType.CONNECTION_BEGIN,
|
||||
EventType.CONNECTION_CONNECTED,
|
||||
EventType.CONNECTION_FAILED
|
||||
"connection.begin",
|
||||
"connection.connected",
|
||||
"connection.failed"
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const groupNames: { [key: string]: string } = {};
|
||||
groupNames[EventType.CLIENT_POKE_RECEIVED] = tr("You received a poke");
|
||||
groupNames[EventType.CLIENT_POKE_SEND] = tr("You send a poke");
|
||||
groupNames[EventType.PRIVATE_MESSAGE_SEND] = tr("You received a private message");
|
||||
groupNames[EventType.PRIVATE_MESSAGE_RECEIVED] = tr("You send a private message");
|
||||
const groupNames: { [T in keyof TypeInfo]?: string } = {};
|
||||
groupNames["client.poke.received"] = tr("You received a poke");
|
||||
groupNames["client.poke.send"] = tr("You send a poke");
|
||||
groupNames["private.message.send"] = tr("You received a private message");
|
||||
groupNames["private.message.received"] = tr("You send a private message");
|
||||
|
||||
groupNames[EventType.CLIENT_VIEW_ENTER] = tr("A client enters your view");
|
||||
groupNames[EventType.CLIENT_VIEW_ENTER_OWN_CHANNEL] = tr("A client enters your view and your channel");
|
||||
groupNames["client.view.enter"] = tr("A client enters your view");
|
||||
groupNames["client.view.enter.own.channel"] = tr("A client enters your view and your channel");
|
||||
|
||||
groupNames[EventType.CLIENT_VIEW_MOVE] = tr("A client switches/gets moved/kicked");
|
||||
groupNames[EventType.CLIENT_VIEW_MOVE_OWN_CHANNEL] = tr("A client switches/gets moved/kicked in to/out of your channel");
|
||||
groupNames[EventType.CLIENT_VIEW_MOVE_OWN] = tr("You've been moved or kicked");
|
||||
groupNames["client.view.move"] = tr("A client switches/gets moved/kicked");
|
||||
groupNames["client.view.move.own.channel"] = tr("A client switches/gets moved/kicked in to/out of your channel");
|
||||
groupNames["client.view.move.own"] = tr("You've been moved or kicked");
|
||||
|
||||
groupNames[EventType.CLIENT_VIEW_LEAVE] = tr("A client leaves/disconnects of your view");
|
||||
groupNames[EventType.CLIENT_VIEW_LEAVE_OWN_CHANNEL] = tr("A client leaves/disconnects of your channel");
|
||||
groupNames["client.view.leave"] = tr("A client leaves/disconnects of your view");
|
||||
groupNames["client.view.leave.own.channel"] = tr("A client leaves/disconnects of your channel");
|
||||
|
||||
groupNames[EventType.GLOBAL_MESSAGE] = tr("A server message has been send");
|
||||
groupNames[EventType.SERVER_CLOSED] = tr("The server has been closed");
|
||||
groupNames[EventType.SERVER_BANNED] = tr("You've been banned from the server");
|
||||
groupNames["global.message"] = tr("A server message has been send");
|
||||
groupNames["server.closed"] = tr("The server has been closed");
|
||||
groupNames["server.banned"] = tr("You've been banned from the server");
|
||||
|
||||
groupNames[EventType.CONNECTION_BEGIN] = tr("You're connecting to a server");
|
||||
groupNames[EventType.CONNECTION_CONNECTED] = tr("You've successfully connected to the server");
|
||||
groupNames[EventType.CONNECTION_FAILED] = tr("You're connect attempt failed");
|
||||
groupNames["connection.begin"] = tr("You're connecting to a server");
|
||||
groupNames["connection.connected"] = tr("You've successfully connected to the server");
|
||||
groupNames["connection.failed"] = tr("You're connect attempt failed");
|
||||
|
||||
function initializeController(events: Registry<NotificationSettingsEvents>) {
|
||||
let filter = undefined;
|
||||
|
|
|
@ -258,6 +258,8 @@ class ChannelTreeController {
|
|||
this.sendChannelInfo(event.newChannel);
|
||||
this.sendChannelStatusIcon(event.newChannel);
|
||||
this.sendChannelTreeEntries();
|
||||
|
||||
this.sendClientTalkStatus(event.client);
|
||||
}
|
||||
|
||||
@EventHandler<ChannelTreeEvents>("notify_selected_entry_changed")
|
||||
|
|
|
@ -14,7 +14,6 @@ import * as log from "tc-shared/log";
|
|||
import {LogCategory, logDebug, logError, logTrace} from "tc-shared/log";
|
||||
import {Regex} from "tc-shared/ui/modal/ModalConnect";
|
||||
import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
|
||||
import {EventType} from "tc-shared/ui/frames/log/Definitions";
|
||||
import {WrappedWebSocket} from "tc-backend/web/connection/WrappedWebSocket";
|
||||
import {AbstractVoiceConnection} from "tc-shared/connection/VoiceConnection";
|
||||
import {parseCommand} from "tc-backend/web/connection/CommandParser";
|
||||
|
@ -27,7 +26,6 @@ import {ServerFeature} from "tc-shared/connection/ServerFeatures";
|
|||
import {RTCConnection} from "tc-shared/connection/rtc/Connection";
|
||||
import {RtpVideoConnection} from "tc-shared/connection/rtc/video/Connection";
|
||||
import { tr } from "tc-shared/i18n/localize";
|
||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||
|
||||
class ReturnListener<T> {
|
||||
resolve: (value?: T | PromiseLike<T>) => void;
|
||||
|
@ -286,7 +284,7 @@ export class ServerConnection extends AbstractServerConnection {
|
|||
|
||||
private startHandshake() {
|
||||
this.updateConnectionState(ConnectionState.INITIALISING);
|
||||
this.client.log.log(EventType.CONNECTION_LOGIN, {});
|
||||
this.client.log.log("connection.login", {});
|
||||
this.handshakeHandler.initialize();
|
||||
this.handshakeHandler.startHandshake();
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import {ConnectionStatistics, ServerConnectionEvents} from "tc-shared/connection
|
|||
import {ConnectionState} from "tc-shared/ConnectionHandler";
|
||||
import {VoiceBridge, VoicePacket, VoiceWhisperPacket} from "./bridge/VoiceBridge";
|
||||
import {NativeWebRTCVoiceBridge} from "./bridge/NativeWebRTCVoiceBridge";
|
||||
import {EventType} from "tc-shared/ui/frames/log/Definitions";
|
||||
import {
|
||||
kUnknownWhisperClientUniqueId,
|
||||
WhisperSession,
|
||||
|
@ -191,7 +190,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
|||
}, payload)))
|
||||
};
|
||||
this.voiceBridge.callbackDisconnect = () => {
|
||||
this.connection.client.log.log(EventType.CONNECTION_VOICE_DROPPED, { });
|
||||
this.connection.client.log.log("connection.voice.dropped", { });
|
||||
if(!this.connectionLostModalOpen) {
|
||||
this.connectionLostModalOpen = true;
|
||||
const modal = createErrorModal(tr("Voice connection lost"), tr("Lost voice connection to the target server. Trying to reconnect..."));
|
||||
|
@ -202,14 +201,14 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
|||
this.executeVoiceBridgeReconnect();
|
||||
}
|
||||
|
||||
this.connection.client.log.log(EventType.CONNECTION_VOICE_CONNECT, { attemptCount: this.connectAttemptCounter });
|
||||
this.connection.client.log.log("connection.voice.connect", { attemptCount: this.connectAttemptCounter });
|
||||
this.setConnectionState(VoiceConnectionStatus.Connecting);
|
||||
this.voiceBridge.connect().then(result => {
|
||||
if(result.type === "success") {
|
||||
this.lastConnectAttempt = 0;
|
||||
this.connectAttemptCounter = 0;
|
||||
|
||||
this.connection.client.log.log(EventType.CONNECTION_VOICE_CONNECT_SUCCEEDED, { });
|
||||
this.connection.client.log.log("connection.voice.connect.succeeded", { });
|
||||
const currentInput = this.voiceRecorder()?.input;
|
||||
if(currentInput) {
|
||||
this.voiceBridge.setInput(currentInput).catch(error => {
|
||||
|
@ -226,7 +225,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
|||
let doReconnect = result.allowReconnect && this.connectAttemptCounter < 5;
|
||||
logWarn(LogCategory.VOICE, tr("Failed to setup voice bridge: %s. Reconnect: %o"), result.message, doReconnect);
|
||||
|
||||
this.connection.client.log.log(EventType.CONNECTION_VOICE_CONNECT_FAILED, {
|
||||
this.connection.client.log.log("connection.voice.connect.failed", {
|
||||
reason: result.message,
|
||||
reconnect_delay: doReconnect ? 1 : 0
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue