A lot of updates and added a new feature

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

View File

@ -1,4 +1,11 @@
# Changelog:
* **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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import {EventType} from "../../../ui/frames/log/Definitions";
import {Settings, settings} from "../../../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") {

View File

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

View File

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

View File

@ -59,7 +59,6 @@ export abstract class AbstractChat<Events extends AbstractConversationEvents> {
protected errorMessage: string;
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 });
}
}

View File

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

View File

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

View File

@ -9,3 +9,5 @@ export const rendererHTML = new HTMLRenderer(rendererReact);
import "./emoji";
import "./highlight";
import "./youtube";
import "./url";
import "./image";

View File

@ -4,8 +4,10 @@ import * as loader from "tc-loader";
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
import {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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,348 +1,16 @@
import {PermissionInfo} from "../../../permission/PermissionManager";
import {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
}
}

View File

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

View File

@ -1,12 +1,17 @@
import {LogMessage, ServerLogUIEvents} from "tc-shared/ui/frames/log/Definitions";
import {VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
import {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>
);
}

View File

@ -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>&nbsp;{data.message}</> : undefined;
@ -544,11 +544,11 @@ registerDispatcher(EventType.SERVER_BANNED,(data, handlerId) => {
);
});
registerDispatcher(EventType.SERVER_REQUIRES_PASSWORD,() => (
registerRenderer(EventType.SERVER_REQUIRES_PASSWORD,() => (
<Translatable>Server requires a password to connect.</Translatable>
));
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);

View File

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

View File

@ -29,8 +29,6 @@ export abstract class AbstractConversationController<
protected currentSelectedConversation: ConversationType;
protected 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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,6 @@ import {
ChannelConversationManager,
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")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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