A lot of updated related to the menu bar and the basic event handler system

canary
WolverinDEV 2020-04-09 15:10:14 +02:00
parent cccd780724
commit f7d6671457
22 changed files with 744 additions and 495 deletions

View File

@ -1,4 +1,10 @@
# Changelog: # Changelog:
* **09.03.20**
- Using React for the client control bar
- Saving last away state and message
- Saving last query show state
- Removing the hostbutton when we're disconnected from the server
* **04.03.20** * **04.03.20**
- Implemented the new music bot playlist song list - Implemented the new music bot playlist song list
- Implemented the missing server log message builders - Implemented the missing server log message builders

View File

@ -2,15 +2,14 @@ import {ChannelTree} from "tc-shared/ui/view";
import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase"; import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase";
import {PermissionManager} from "tc-shared/permission/PermissionManager"; import {PermissionManager} from "tc-shared/permission/PermissionManager";
import {GroupManager} from "tc-shared/permission/GroupManager"; import {GroupManager} from "tc-shared/permission/GroupManager";
import {ServerSettings, Settings, StaticSettings} from "tc-shared/settings"; import {ServerSettings, Settings, settings, StaticSettings} from "tc-shared/settings";
import {Sound, SoundManager} from "tc-shared/sound/Sounds"; import {Sound, SoundManager} from "tc-shared/sound/Sounds";
import {LocalClientEntry} from "tc-shared/ui/client"; import {LocalClientEntry} from "tc-shared/ui/client";
import {ServerLog} from "tc-shared/ui/frames/server_log"; import * as server_log from "tc-shared/ui/frames/server_log";
import {ConnectionProfile, default_profile, find_profile} from "tc-shared/profiles/ConnectionProfile"; import {ConnectionProfile, default_profile, find_profile} from "tc-shared/profiles/ConnectionProfile";
import {ServerAddress} from "tc-shared/ui/server"; import {ServerAddress} from "tc-shared/ui/server";
import * as log from "tc-shared/log"; import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log"; import {LogCategory} from "tc-shared/log";
import * as server_log from "tc-shared/ui/frames/server_log";
import {createErrorModal, createInfoModal, createInputModal, Modal} from "tc-shared/ui/elements/Modal"; import {createErrorModal, createInfoModal, createInputModal, Modal} from "tc-shared/ui/elements/Modal";
import {hashPassword} from "tc-shared/utils/helpers"; import {hashPassword} from "tc-shared/utils/helpers";
import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler"; import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
@ -31,7 +30,8 @@ import {spawnAvatarUpload} from "tc-shared/ui/modal/ModalAvatar";
import * as connection from "tc-backend/connection"; import * as connection from "tc-backend/connection";
import * as dns from "tc-backend/dns"; import * as dns from "tc-backend/dns";
import * as top_menu from "tc-shared/ui/frames/MenuBar"; import * as top_menu from "tc-shared/ui/frames/MenuBar";
import {control_bar_instance} from "tc-shared/ui/frames/control-bar"; import {EventHandler, Registry} from "tc-shared/events";
import {ServerLog} from "tc-shared/ui/frames/server_log";
export enum DisconnectReason { export enum DisconnectReason {
HANDLER_DESTROYED, HANDLER_DESTROYED,
@ -54,11 +54,25 @@ export enum DisconnectReason {
} }
export enum ConnectionState { export enum ConnectionState {
UNCONNECTED, UNCONNECTED, /* no connection is currenting running */
CONNECTING, CONNECTING, /* we try to establish a connection to the target server */
INITIALISING, INITIALISING, /* we're setting up the connection encryption */
CONNECTED, AUTHENTICATING, /* we're authenticating ourself so we get a unique ID */
DISCONNECTING CONNECTED, /* we're connected to the server. Server init has been done, may not everything is initialized */
DISCONNECTING/* we're curently disconnecting from the server and awaiting disconnect acknowledge */
}
export namespace ConnectionState {
export function socket_connected(state: ConnectionState) {
switch (state) {
case ConnectionState.CONNECTED:
case ConnectionState.AUTHENTICATING:
//case ConnectionState.INITIALISING: /* its not yet possible to send any data */
return true;
default:
return false;
}
}
} }
export enum ViewReasonId { export enum ViewReasonId {
@ -76,7 +90,7 @@ export enum ViewReasonId {
VREASON_SERVER_SHUTDOWN = 11 VREASON_SERVER_SHUTDOWN = 11
} }
export interface VoiceStatus { export interface LocalClientStatus {
input_hardware: boolean; input_hardware: boolean;
input_muted: boolean; input_muted: boolean;
output_muted: boolean; output_muted: boolean;
@ -106,6 +120,7 @@ export interface ConnectParameters {
declare const native_client; declare const native_client;
export class ConnectionHandler { export class ConnectionHandler {
private readonly event_registry: Registry<ConnectionEvents>;
channelTree: ChannelTree; channelTree: ChannelTree;
serverConnection: AbstractServerConnection; serverConnection: AbstractServerConnection;
@ -132,7 +147,7 @@ export class ConnectionHandler {
private _connect_initialize_id: number = 1; private _connect_initialize_id: number = 1;
client_status: VoiceStatus = { private client_status: LocalClientStatus = {
input_hardware: false, input_hardware: false,
input_muted: false, input_muted: false,
output_muted: false, output_muted: false,
@ -150,6 +165,9 @@ export class ConnectionHandler {
log: ServerLog; log: ServerLog;
constructor() { constructor() {
this.event_registry = new Registry<ConnectionEvents>();
this.event_registry.enable_debug("connection-handler");
this.settings = new ServerSettings(); this.settings = new ServerSettings();
this.log = new ServerLog(this); this.log = new ServerLog(this);
@ -178,14 +196,30 @@ export class ConnectionHandler {
if(event.isDefaultPrevented()) if(event.isDefaultPrevented())
return; return;
server_connections.set_active_connection_handler(this); server_connections.set_active_connection(this);
}); });
this.tag_connection_handler.find(".button-close").on('click', event => { this.tag_connection_handler.find(".button-close").on('click', event => {
server_connections.destroy_server_connection_handler(this); server_connections.destroy_server_connection(this);
event.preventDefault(); event.preventDefault();
}); });
this.tab_set_name(tr("Not connected")); this.tab_set_name(tr("Not connected"));
} }
this.event_registry.register_handler(this);
}
initialize_client_state(source?: ConnectionHandler) {
this.client_status.input_muted = source ? source.client_status.input_muted : settings.global(Settings.KEY_CLIENT_STATE_MICROPHONE_MUTED);
this.client_status.output_muted = source ? source.client_status.output_muted : settings.global(Settings.KEY_CLIENT_STATE_SPEAKER_MUTED);
this.update_voice_status();
this.setSubscribeToAllChannels(source ? source.client_status.channel_subscribe_all : settings.global(Settings.KEY_CLIENT_STATE_SUBSCRIBE_ALL_CHANNELS));
this.setAway_(source ? source.client_status.away : (settings.global(Settings.KEY_CLIENT_STATE_AWAY) ? settings.global(Settings.KEY_CLIENT_AWAY_MESSAGE) : true), false);
this.setQueriesShown(source ? source.client_status.queries_visible : settings.global(Settings.KEY_CLIENT_STATE_QUERY_SHOWN));
}
events() : Registry<ConnectionEvents> {
return this.event_registry;
} }
tab_set_name(name: string) { tab_set_name(name: string) {
@ -193,8 +227,6 @@ export class ConnectionHandler {
this.tag_connection_handler.find(".server-name").text(name); this.tag_connection_handler.find(".server-name").text(name);
} }
setup() { }
async startConnection(addr: string, profile: ConnectionProfile, user_action: boolean, parameters: ConnectParameters) { async startConnection(addr: string, profile: ConnectionProfile, user_action: boolean, parameters: ConnectParameters) {
this.tab_set_name(tr("Connecting")); this.tab_set_name(tr("Connecting"));
this.cancel_reconnect(false); this.cancel_reconnect(false);
@ -283,6 +315,17 @@ export class ConnectionHandler {
}, 50); }, 50);
} }
async disconnectFromServer(reason?: string) {
this.cancel_reconnect(true);
this.handleDisconnect(DisconnectReason.REQUESTED); //TODO message?
try {
await this.serverConnection.disconnect();
} catch (error) {
log.warn(LogCategory.CLIENT, tr("Failed to successfully disconnect from server: {}"), error);
}
this.sound.play(Sound.CONNECTION_DISCONNECTED);
this.log.log(server_log.Type.DISCONNECTED, {});
}
getClient() : LocalClientEntry { return this._local_client; } getClient() : LocalClientEntry { return this._local_client; }
getClientId() { return this._clientId; } getClientId() { return this._clientId; }
@ -299,16 +342,20 @@ export class ConnectionHandler {
getServerConnection() : AbstractServerConnection { return this.serverConnection; } getServerConnection() : AbstractServerConnection { return this.serverConnection; }
/** @EventHandler<ConnectionEvents>("notify_connection_state_changed")
* LISTENER private handleConnectionConnected(event: ConnectionEvents["notify_connection_state_changed"]) {
*/ if(event.new_state !== ConnectionState.CONNECTED) return;
onConnected() {
log.info(LogCategory.CLIENT, tr("Client connected")); log.info(LogCategory.CLIENT, tr("Client connected"));
this.log.log(server_log.Type.CONNECTION_CONNECTED, {
own_client: this.getClient().log_data()
});
this.sound.play(Sound.CONNECTION_CONNECTED);
this.permissions.requestPermissionList(); this.permissions.requestPermissionList();
if(this.groups.serverGroups.length == 0) if(this.groups.serverGroups.length == 0)
this.groups.requestGroups(); this.groups.requestGroups();
this.initialize_server_settings(); this.settings.setServer(this.channelTree.server.properties.virtualserver_unique_identifier);
/* apply the server settings */ /* apply the server settings */
if(this.client_status.channel_subscribe_all) if(this.client_status.channel_subscribe_all)
@ -327,27 +374,6 @@ export class ConnectionHandler {
*/ */
} }
private initialize_server_settings() {
let update_control = false;
this.settings.setServer(this.channelTree.server.properties.virtualserver_unique_identifier);
{
const flag_subscribe = this.settings.server(Settings.KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL, true);
if(this.client_status.channel_subscribe_all != flag_subscribe) {
this.client_status.channel_subscribe_all = flag_subscribe;
update_control = true;
}
}
{
const flag_query = this.settings.server(Settings.KEY_CONTROL_SHOW_QUERIES, false);
if(this.client_status.queries_visible != flag_query) {
this.client_status.queries_visible = flag_query;
update_control = true;
}
}
control_bar_instance()?.events().fire("server_updated", { category: "settings-initialized", handler: this });
}
get connected() : boolean { get connected() : boolean {
return this.serverConnection && this.serverConnection.connected(); return this.serverConnection && this.serverConnection.connected();
} }
@ -606,7 +632,6 @@ export class ConnectionHandler {
if(this.serverConnection) if(this.serverConnection)
this.serverConnection.disconnect(); this.serverConnection.disconnect();
this.on_connection_state_changed(); /* really required to call? */
this.side_bar.private_conversations().clear_client_ids(); this.side_bar.private_conversations().clear_client_ids();
this.hostbanner.update(); this.hostbanner.update();
@ -639,12 +664,16 @@ export class ConnectionHandler {
} }
} }
private on_connection_state_changed() { private on_connection_state_changed(old_state: ConnectionState, new_state: ConnectionState) {
control_bar_instance()?.events().fire("server_updated", { category: "connection-state", handler: this }); this.event_registry.fire("notify_connection_state_changed", {
old_state: old_state,
new_state: new_state
});
} }
private _last_record_error_popup: number; private _last_record_error_popup: number;
update_voice_status(targetChannel?: ChannelEntry) { update_voice_status(targetChannel?: ChannelEntry) {
//TODO: Simplify this
if(!this._local_client) return; /* we've been destroyed */ if(!this._local_client) return; /* we've been destroyed */
targetChannel = targetChannel || this.getClient().currentChannel(); targetChannel = targetChannel || this.getClient().currentChannel();
@ -748,9 +777,14 @@ export class ConnectionHandler {
} }
} }
//TODO: Only trigger events for stuff which has been updated
control_bar_instance()?.events().fire("server_updated", { category: "audio", handler: this }); this.event_registry.fire("notify_state_updated", {
top_menu.update_state(); //TODO: Only run "small" update? state: "microphone"
});
this.event_registry.fire("notify_state_updated", {
state: "speaker"
});
top_menu.update_state(); //TODO: Top-Menu should register their listener
} }
sync_status_with_server() { sync_status_with_server() {
@ -768,35 +802,13 @@ export class ConnectionHandler {
}); });
} }
set_away_status(state: boolean | string, update_control_bar: boolean) {
if(this.client_status.away === state)
return;
if(state) {
this.sound.play(Sound.AWAY_ACTIVATED);
} else {
this.sound.play(Sound.AWAY_DEACTIVATED);
}
this.client_status.away = state;
this.serverConnection.send_command("clientupdate", {
client_away: typeof(this.client_status.away) === "string" || this.client_status.away,
client_away_message: typeof(this.client_status.away) === "string" ? this.client_status.away : "",
}).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to update away status. Error: %o"), error);
this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to update away status.")});
});
if(update_control_bar)
control_bar_instance()?.events().fire("server_updated", { category: "away-status", handler: this });
}
resize_elements() { resize_elements() {
this.channelTree.handle_resized(); this.channelTree.handle_resized();
this.invoke_resized_on_activate = false; this.invoke_resized_on_activate = false;
} }
acquire_recorder(voice_recoder: RecorderProfile, update_control_bar: boolean) { acquire_recorder(voice_recoder: RecorderProfile, update_control_bar: boolean) {
/* TODO: If the voice connection hasn't been set upped cache the target recorder */
const vconnection = this.serverConnection.voice_connection(); const vconnection = this.serverConnection.voice_connection();
(vconnection ? vconnection.acquire_voice_recorder(voice_recoder) : Promise.resolve()).catch(error => { (vconnection ? vconnection.acquire_voice_recorder(voice_recoder) : Promise.resolve()).catch(error => {
log.warn(LogCategory.VOICE, tr("Failed to acquire recorder (%o)"), error); log.warn(LogCategory.VOICE, tr("Failed to acquire recorder (%o)"), error);
@ -805,6 +817,8 @@ export class ConnectionHandler {
}); });
} }
getVoiceRecorder() :RecorderProfile | undefined { return this.serverConnection?.voice_connection()?.voice_recorder(); }
reconnect_properties(profile?: ConnectionProfile) : ConnectParameters { reconnect_properties(profile?: ConnectionProfile) : ConnectParameters {
const name = (this.getClient() ? this.getClient().clientNickName() : "") || const name = (this.getClient() ? this.getClient().clientNickName() : "") ||
(this.serverConnection && this.serverConnection.handshake_handler() ? this.serverConnection.handshake_handler().parameters.nickname : "") || (this.serverConnection && this.serverConnection.handshake_handler() ? this.serverConnection.handshake_handler().parameters.nickname : "") ||
@ -913,6 +927,7 @@ export class ConnectionHandler {
} }
destroy() { destroy() {
this.event_registry.unregister_handler(this);
this.cancel_reconnect(true); this.cancel_reconnect(true);
this.tag_connection_handler && this.tag_connection_handler.remove(); this.tag_connection_handler && this.tag_connection_handler.remove();
@ -954,4 +969,107 @@ export class ConnectionHandler {
this.sound = undefined; this.sound = undefined;
this._local_client = undefined; this._local_client = undefined;
} }
/* state changing methods */
setMicrophoneMuted(muted: boolean) {
if(this.client_status.input_muted === muted) return;
this.client_status.input_muted = muted;
this.sound.play(muted ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED);
this.update_voice_status();
}
isMicrophoneMuted() { return this.client_status.input_muted; }
/*
* Returns whatever the client is able to talk or not. Reasons for returning true could be:
* - Channel codec isn't supported
* - No recorder has been acquired
* - Voice bridge hasn't been set upped yet
*/
isMicrophoneDisabled() { return !this.client_status.input_hardware; }
setSpeakerMuted(muted: boolean) {
if(this.client_status.output_muted === muted) return;
if(muted) this.sound.play(Sound.SOUND_MUTED); /* play the sound *before* we're setting the muted state */
this.client_status.output_muted = muted;
if(!muted) this.sound.play(Sound.SOUND_ACTIVATED); /* play the sound *after* we're setting we've unmuted the sound */
this.update_voice_status();
}
isSpeakerMuted() { return this.client_status.output_muted; }
/*
* Returns whatever the client is able to playback sound (voice). Reasons for returning true could be:
* - Channel codec isn't supported
* - Voice bridge hasn't been set upped yet
*/
//TODO: This currently returns false
isSpeakerDisabled() { return false; }
setSubscribeToAllChannels(flag: boolean) {
if(this.client_status.channel_subscribe_all === flag) return;
this.client_status.channel_subscribe_all = flag;
if(flag)
this.channelTree.subscribe_all_channels();
else
this.channelTree.unsubscribe_all_channels();
this.event_registry.fire("notify_state_updated", { state: "subscribe" });
}
isSubscribeToAllChannels() { return this.client_status.channel_subscribe_all; }
setAway(state: boolean | string) {
this.setAway_(state, true);
}
private setAway_(state: boolean | string, play_sound: boolean) {
if(this.client_status.away === state)
return;
const was_away = this.isAway();
const will_away = typeof state === "boolean" ? state : true;
if(was_away != will_away && play_sound)
this.sound.play(will_away ? Sound.AWAY_ACTIVATED : Sound.AWAY_DEACTIVATED);
this.client_status.away = state;
this.serverConnection.send_command("clientupdate", {
client_away: typeof(this.client_status.away) === "string" || this.client_status.away,
client_away_message: typeof(this.client_status.away) === "string" ? this.client_status.away : "",
}).catch(error => {
log.warn(LogCategory.GENERAL, tr("Failed to update away status. Error: %o"), error);
this.log.log(server_log.Type.ERROR_CUSTOM, {message: tr("Failed to update away status.")});
});
this.event_registry.fire("notify_state_updated", {
state: "away"
});
}
isAway() : boolean { return typeof this.client_status.away !== "boolean" || this.client_status.away; }
setQueriesShown(flag: boolean) {
if(this.client_status.queries_visible === flag) return;
this.client_status.queries_visible = flag;
this.channelTree.toggle_server_queries(flag);
this.event_registry.fire("notify_state_updated", {
state: "query"
});
}
areQueriesShown() {
return this.client_status.queries_visible;
}
}
export type ConnectionStateUpdateType = "microphone" | "speaker" | "away" | "subscribe" | "query";
export interface ConnectionEvents {
notify_state_updated: {
state: ConnectionStateUpdateType;
}
notify_connection_state_changed: {
old_state: ConnectionState,
new_state: ConnectionState
}
} }

View File

@ -5,14 +5,15 @@ import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/
import {default_profile, find_profile} from "tc-shared/profiles/ConnectionProfile"; import {default_profile, find_profile} from "tc-shared/profiles/ConnectionProfile";
import {server_connections} from "tc-shared/ui/frames/connection_handlers"; import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect"; import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
import {control_bar} from "tc-shared/ui/frames/ControlBar";
import * as top_menu from "./ui/frames/MenuBar"; import * as top_menu from "./ui/frames/MenuBar";
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => { export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => {
const profile = find_profile(mark.connect_profile) || default_profile(); const profile = find_profile(mark.connect_profile) || default_profile();
if(profile.valid()) { if(profile.valid()) {
const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection_handler() : server_connections.spawn_server_connection_handler(); const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection() : server_connections.spawn_server_connection();
server_connections.set_active_connection_handler(connection); server_connections.set_active_connection(connection);
connection.startConnection( connection.startConnection(
mark.server_properties.server_address + ":" + mark.server_properties.server_port, mark.server_properties.server_address + ":" + mark.server_properties.server_port,
profile, profile,
@ -232,23 +233,23 @@ export function delete_bookmark(bookmark: Bookmark | DirectoryBookmark) {
delete_bookmark_recursive(bookmarks(), bookmark) delete_bookmark_recursive(bookmarks(), bookmark)
} }
export function add_current_server() { export function add_server_to_bookmarks(server: ConnectionHandler) {
const ch = server_connections.active_connection_handler(); if(server && server.connected) {
if(ch && ch.connected) { const ce = server.getClient();
const ce = ch.getClient();
const name = ce ? ce.clientNickName() : undefined; const name = ce ? ce.clientNickName() : undefined;
createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:<br>"), text => text.length > 0, result => { createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:<br>"), text => text.length > 0, result => {
if(result) { if(result) {
const bookmark = create_bookmark(result as string, bookmarks(), { const bookmark = create_bookmark(result as string, bookmarks(), {
server_port: ch.serverConnection.remote_address().port, server_port: server.serverConnection.remote_address().port,
server_address: ch.serverConnection.remote_address().host, server_address: server.serverConnection.remote_address().host,
server_password: "", server_password: "",
server_password_hash: "" server_password_hash: ""
}, name); }, name);
save_bookmark(bookmark); save_bookmark(bookmark);
control_bar.update_bookmarks(); control_bar_instance().events().fire("update_state", { state: "bookmarks" });
//control_bar.update_bookmarks();
top_menu.rebuild_bookmarks(); top_menu.rebuild_bookmarks();
createInfoModal(tr("Server added"), tr("Server has been successfully added to your bookmarks.")).open(); createInfoModal(tr("Server added"), tr("Server has been successfully added to your bookmarks.")).open();

View File

@ -1,11 +1,9 @@
import * as log from "tc-shared/log"; import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import * as server_log from "tc-shared/ui/frames/server_log"; import * as server_log from "tc-shared/ui/frames/server_log";
import { import {AbstractServerConnection, CommandOptions, ServerCommand} from "tc-shared/connection/ConnectionBase";
AbstractServerConnection, CommandOptions, ServerCommand
} from "tc-shared/connection/ConnectionBase";
import {Sound} from "tc-shared/sound/Sounds"; import {Sound} from "tc-shared/sound/Sounds";
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration"; import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
import {LogCategory} from "tc-shared/log";
import {createErrorModal, createInfoModal, createInputModal, createModal} from "tc-shared/ui/elements/Modal"; import {createErrorModal, createInfoModal, createInputModal, createModal} from "tc-shared/ui/elements/Modal";
import { import {
ClientConnectionInfo, ClientConnectionInfo,
@ -16,7 +14,7 @@ import {
SongInfo SongInfo
} from "tc-shared/ui/client"; } from "tc-shared/ui/client";
import {ChannelEntry} from "tc-shared/ui/channel"; import {ChannelEntry} from "tc-shared/ui/channel";
import {ConnectionHandler, DisconnectReason, ViewReasonId} from "tc-shared/ConnectionHandler"; import {ConnectionHandler, ConnectionState, DisconnectReason, ViewReasonId} from "tc-shared/ConnectionHandler";
import {bbcode_chat, formatMessage} from "tc-shared/ui/frames/chat"; import {bbcode_chat, formatMessage} from "tc-shared/ui/frames/chat";
import {server_connections} from "tc-shared/ui/frames/connection_handlers"; import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {spawnPoke} from "tc-shared/ui/modal/ModalPoke"; import {spawnPoke} from "tc-shared/ui/modal/ModalPoke";
@ -247,7 +245,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
if(properties.virtualserver_ask_for_privilegekey) { if(properties.virtualserver_ask_for_privilegekey) {
createInputModal(tr("Use a privilege key"), tr("This is a newly created server for which administrator privileges have not yet been claimed.<br>Please enter the \"privilege key\" that was automatically generated when this server was created to gain administrator permissions."), message => message.length > 0, result => { createInputModal(tr("Use a privilege key"), tr("This is a newly created server for which administrator privileges have not yet been claimed.<br>Please enter the \"privilege key\" that was automatically generated when this server was created to gain administrator permissions."), message => message.length > 0, result => {
if(!result) return; if(!result) return;
const scon = server_connections.active_connection_handler(); const scon = server_connections.active_connection();
if(scon.serverConnection.connected) if(scon.serverConnection.connected)
scon.serverConnection.send_command("tokenuse", { scon.serverConnection.send_command("tokenuse", {
@ -260,11 +258,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
}, { field_placeholder: tr("Enter Privilege Key") }).open(); }, { field_placeholder: tr("Enter Privilege Key") }).open();
} }
this.connection_handler.log.log(server_log.Type.CONNECTION_CONNECTED, { this.connection.updateConnectionState(ConnectionState.CONNECTED);
own_client: this.connection_handler.getClient().log_data()
});
this.connection_handler.sound.play(Sound.CONNECTION_CONNECTED);
this.connection.client.onConnected();
} }
handleNotifyServerConnectionInfo(json) { handleNotifyServerConnectionInfo(json) {

View File

@ -47,6 +47,9 @@ export abstract class AbstractServerConnection {
abstract remote_address() : ServerAddress; /* only valid when connected */ abstract remote_address() : ServerAddress; /* only valid when connected */
abstract handshake_handler() : HandshakeHandler; /* only valid when connected */ abstract handshake_handler() : HandshakeHandler; /* only valid when connected */
//FIXME: Remove this this is currently only some kind of hack
abstract updateConnectionState(state: ConnectionState);
abstract ping() : { abstract ping() : {
native: number, native: number,
javascript?: number javascript?: number

View File

@ -110,6 +110,7 @@ export class Registry<Events> {
fire<T extends keyof Events>(event_type: T, data?: Events[T]) { fire<T extends keyof Events>(event_type: T, data?: Events[T]) {
if(this.debug_prefix) console.log("[%s] Trigger event: %s", this.debug_prefix, event_type); if(this.debug_prefix) console.log("[%s] Trigger event: %s", this.debug_prefix, event_type);
if(typeof data === "object" && 'type' in data) throw tr("The keyword 'type' is reserved for the event type and should not be passed as argument");
const event = Object.assign(typeof data === "undefined" ? SingletonEvent.instance : data, { const event = Object.assign(typeof data === "undefined" ? SingletonEvent.instance : data, {
type: event_type, type: event_type,
as: function () { return this; } as: function () { return this; }
@ -130,7 +131,7 @@ export class Registry<Events> {
invoke_count++; invoke_count++;
} }
if(invoke_count === 0) { if(invoke_count === 0) {
console.warn("Event handler (%s) triggered event %s which has no consumers.", this.debug_prefix, event_type); console.warn(tr("Event handler (%s) triggered event %s which has no consumers."), this.debug_prefix, event_type);
} }
} }
@ -154,6 +155,7 @@ export class Registry<Events> {
let registered_events = {}; let registered_events = {};
for(const function_name of Object.getOwnPropertyNames(proto)) { for(const function_name of Object.getOwnPropertyNames(proto)) {
if(function_name === "constructor") continue; if(function_name === "constructor") continue;
if(typeof proto[function_name] !== "function") continue;
if(typeof proto[function_name][event_annotation_key] !== "object") continue; if(typeof proto[function_name][event_annotation_key] !== "object") continue;
const event_data = proto[function_name][event_annotation_key]; const event_data = proto[function_name][event_annotation_key];

View File

@ -7,7 +7,7 @@ import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal"; import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal";
import {default_recorder} from "tc-shared/voice/RecorderProfile"; import {default_recorder} from "tc-shared/voice/RecorderProfile";
import {Settings, settings} from "tc-shared/settings"; import {Settings, settings} from "tc-shared/settings";
import {add_current_server} from "tc-shared/bookmarks"; import {add_server_to_bookmarks} from "tc-shared/bookmarks";
import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect"; import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
import PermissionType from "tc-shared/permission/PermissionType"; import PermissionType from "tc-shared/permission/PermissionType";
import {spawnQueryCreate} from "tc-shared/ui/modal/ModalQuery"; import {spawnQueryCreate} from "tc-shared/ui/modal/ModalQuery";
@ -17,6 +17,7 @@ import {formatMessage} from "tc-shared/ui/frames/chat";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings"; import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
/*
function initialize_sounds(event_registry: Registry<ClientGlobalControlEvents>) { function initialize_sounds(event_registry: Registry<ClientGlobalControlEvents>) {
{ {
let microphone_muted = undefined; let microphone_muted = undefined;
@ -38,102 +39,16 @@ function initialize_sounds(event_registry: Registry<ClientGlobalControlEvents>)
} }
} }
function load_default_states() { export function load_default_states(event_registry: Registry<ClientGlobalControlEvents>) {
this.event_registry.fire("action_toggle_speaker", { state: settings.static_global(Settings.KEY_CONTROL_MUTE_OUTPUT, false) }); event_registry.fire("action_toggle_speaker", { state: settings.static_global(Settings.KEY_CONTROL_MUTE_OUTPUT, false) });
this.event_registry.fire("action_toggle_microphone", { state: settings.static_global(Settings.KEY_CONTROL_MUTE_INPUT, false) }); event_registry.fire("action_toggle_microphone", { state: settings.static_global(Settings.KEY_CONTROL_MUTE_INPUT, false) });
} }
*/
export function initialize(event_registry: Registry<ClientGlobalControlEvents>) { export function initialize(event_registry: Registry<ClientGlobalControlEvents>) {
let current_connection_handler: ConnectionHandler | undefined; let current_connection_handler: ConnectionHandler | undefined;
event_registry.on("action_set_active_connection_handler", event => { current_connection_handler = event.handler; }); server_connections.events().on("notify_active_handler_changed", event => current_connection_handler = event.new_handler);
//initialize_sounds(event_registry);
initialize_sounds(event_registry);
/* away state handler */
event_registry.on("action_set_away", event => {
const set_away = message => {
for(const connection of event.globally ? server_connections.server_connection_handlers() : [server_connections.active_connection_handler()]) {
if(!connection) continue;
connection.set_away_status(typeof message === "string" && !!message ? message : true, false);
}
control_bar_instance()?.events()?.fire("update_state", { state: "away" });
};
if(event.prompt_reason) {
createInputModal(tr("Set away message"), tr("Please enter your away message"), () => true, message => {
if(typeof(message) === "string")
set_away(message);
}).open();
} else {
set_away(undefined);
}
});
event_registry.on("action_disable_away", event => {
for(const connection of event.globally ? server_connections.server_connection_handlers() : [server_connections.active_connection_handler()]) {
if(!connection) continue;
connection.set_away_status(false, false);
}
control_bar_instance()?.events()?.fire("update_state", { state: "away" });
});
event_registry.on("action_toggle_microphone", event => {
/* just update the last changed value */
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_INPUT, !event.state);
if(current_connection_handler) {
current_connection_handler.client_status.input_muted = !event.state;
if(!current_connection_handler.client_status.input_hardware)
current_connection_handler.acquire_recorder(default_recorder, true); /* acquire_recorder already updates the voice status */
else
current_connection_handler.update_voice_status(undefined);
}
});
event_registry.on("action_toggle_speaker", event => {
/* just update the last changed value */
settings.changeGlobal(Settings.KEY_CONTROL_MUTE_OUTPUT, !event.state);
if(!current_connection_handler) return;
current_connection_handler.client_status.output_muted = !event.state;
current_connection_handler.update_voice_status(undefined);
});
event_registry.on("action_set_channel_subscribe_mode", event => {
if(!current_connection_handler) return;
current_connection_handler.client_status.channel_subscribe_all = event.subscribe;
if(event.subscribe)
current_connection_handler.channelTree.subscribe_all_channels();
else
current_connection_handler.channelTree.unsubscribe_all_channels(true);
current_connection_handler.settings.changeServer(Settings.KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL, event.subscribe);
});
event_registry.on("action_toggle_query", event => {
if(!current_connection_handler) return;
current_connection_handler.client_status.queries_visible = event.shown;
current_connection_handler.channelTree.toggle_server_queries(event.shown);
current_connection_handler.settings.changeServer(Settings.KEY_CONTROL_SHOW_QUERIES, event.shown);
});
event_registry.on("action_add_current_server_to_bookmarks", () => add_current_server());
event_registry.on("action_open_connect", event => {
current_connection_handler?.cancel_reconnect(true);
spawnConnectModal({
default_connect_new_tab: event.new_tab
}, {
url: "ts.TeaSpeak.de",
enforce: false
});
});
event_registry.on("action_open_window", event => { event_registry.on("action_open_window", event => {
const handle_import_error = error => { const handle_import_error = error => {
@ -233,5 +148,9 @@ export function initialize(event_registry: Registry<ClientGlobalControlEvents>)
} }
}); });
load_default_states(); event_registry.on("action_open_window_connect", event => {
spawnConnectModal({
default_connect_new_tab: event.new_tab
});
});
} }

View File

@ -1,49 +1,17 @@
import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {Registry} from "tc-shared/events";
export interface ClientGlobalControlEvents { export interface ClientGlobalControlEvents {
action_set_channel_subscribe_mode: { /* open a basic window */
subscribe: boolean
},
action_disconnect: {
globally: boolean
},
action_open_connect: {
new_tab: boolean
},
action_toggle_microphone: {
state: boolean
},
action_toggle_speaker: {
state: boolean
},
action_disable_away: {
globally: boolean
},
action_set_away: {
globally: boolean;
prompt_reason: boolean;
},
action_toggle_query: {
shown: boolean
},
action_open_window: { action_open_window: {
window: "bookmark-manage" | "query-manage" | "query-create" | "ban-list" | "permissions" | "token-list" | "token-use" | "settings", window: "bookmark-manage" | "query-manage" | "query-create" | "ban-list" | "permissions" | "token-list" | "token-use" | "settings",
connection?: ConnectionHandler connection?: ConnectionHandler
}, },
action_add_current_server_to_bookmarks: {}, /* some more specific window openings */
action_set_active_connection_handler: { action_open_window_connect: {
handler?: ConnectionHandler new_tab: boolean
},
//TODO
notify_microphone_state_changed: {
state: boolean
} }
} }
export const global_client_actions = new Registry<ClientGlobalControlEvents>();

View File

@ -15,7 +15,7 @@ import * as stats from "./stats";
import * as fidentity from "./profiles/identities/TeaForumIdentity"; import * as fidentity from "./profiles/identities/TeaForumIdentity";
import {default_recorder, RecorderProfile, set_default_recorder} from "tc-shared/voice/RecorderProfile"; import {default_recorder, RecorderProfile, set_default_recorder} from "tc-shared/voice/RecorderProfile";
import * as cmanager from "tc-shared/ui/frames/connection_handlers"; import * as cmanager from "tc-shared/ui/frames/connection_handlers";
import {server_connections, ServerConnectionManager} from "tc-shared/ui/frames/connection_handlers"; import {server_connections, ConnectionManager} from "tc-shared/ui/frames/connection_handlers";
import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect"; import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
import * as top_menu from "./ui/frames/MenuBar"; import * as top_menu from "./ui/frames/MenuBar";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
@ -28,8 +28,9 @@ import * as ppt from "tc-backend/ppt";
import * as React from "react"; import * as React from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import * as cbar from "./ui/frames/control-bar"; import * as cbar from "./ui/frames/control-bar";
import * as global_ev_handler from "./events/ClientGlobalControlHandler";
import {Registry} from "tc-shared/events"; import {Registry} from "tc-shared/events";
import {ClientGlobalControlEvents} from "tc-shared/events/GlobalEvents"; import {ClientGlobalControlEvents, global_client_actions} from "tc-shared/events/GlobalEvents";
/* required import for init */ /* required import for init */
require("./proto").initialize(); require("./proto").initialize();
@ -52,14 +53,14 @@ function setup_close() {
profiles.save(); profiles.save();
if(!settings.static(Settings.KEY_DISABLE_UNLOAD_DIALOG, false)) { if(!settings.static(Settings.KEY_DISABLE_UNLOAD_DIALOG, false)) {
const active_connections = server_connections.server_connection_handlers().filter(e => e.connected); const active_connections = server_connections.all_connections().filter(e => e.connected);
if(active_connections.length == 0) return; if(active_connections.length == 0) return;
if(!native_client) { if(!native_client) {
event.returnValue = "Are you really sure?<br>You're still connected!"; event.returnValue = "Are you really sure?<br>You're still connected!";
} else { } else {
const do_exit = () => { const do_exit = () => {
const dp = server_connections.server_connection_handlers().map(e => { const dp = server_connections.all_connections().map(e => {
if(e.serverConnection.connected()) if(e.serverConnection.connected())
return e.serverConnection.disconnect(tr("client closed")); return e.serverConnection.disconnect(tr("client closed"));
return Promise.resolve(); return Promise.resolve();
@ -146,8 +147,6 @@ async function initialize() {
bipc.setup(); bipc.setup();
} }
export let client_control_events: Registry<ClientGlobalControlEvents>;
async function initialize_app() { async function initialize_app() {
try { //Initialize main template try { //Initialize main template
const main = $("#tmpl_main").renderTag({ const main = $("#tmpl_main").renderTag({
@ -161,16 +160,22 @@ async function initialize_app() {
loader.critical_error(tr("Failed to setup main page!")); loader.critical_error(tr("Failed to setup main page!"));
return; return;
} }
cmanager.initialize();
client_control_events = new Registry<ClientGlobalControlEvents>(); global_ev_handler.initialize(global_client_actions);
{ {
const bar = ( const bar = (
<cbar.ControlBar ref={cbar.react_reference()} multiSession={true} /> <cbar.ControlBar ref={cbar.react_reference()} multiSession={true} />
); );
ReactDOM.render(bar, $(".container-control-bar")[0]); ReactDOM.render(bar, $(".container-control-bar")[0]);
cbar.control_bar_instance().load_default_states();
} }
/*
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "settings init",
priority: 10,
function: async () => global_ev_handler.load_default_states(client_control_events)
});
*/
if(!aplayer.initialize()) if(!aplayer.initialize())
console.warn(tr("Failed to initialize audio controller!")); console.warn(tr("Failed to initialize audio controller!"));
@ -266,7 +271,7 @@ export function handle_connect_request(properties: bipc.connect.ConnectRequestDa
hashed: password_hashed hashed: password_hashed
} : undefined } : undefined
}); });
server_connections.set_active_connection_handler(connection); server_connections.set_active_connection(connection);
} else { } else {
spawnConnectModal({},{ spawnConnectModal({},{
url: properties.address, url: properties.address,
@ -311,12 +316,9 @@ function main() {
top_menu.initialize(); top_menu.initialize();
cmanager.initialize(new ServerConnectionManager($("#connection-handlers"))); const initial_handler = server_connections.spawn_server_connection();
control_bar.control_bar.initialise(); /* before connection handler to allow property apply */
const initial_handler = server_connections.spawn_server_connection_handler();
initial_handler.acquire_recorder(default_recorder, false); initial_handler.acquire_recorder(default_recorder, false);
control_bar.control_bar.set_connection_handler(initial_handler); cmanager.server_connections.set_active_connection(initial_handler);
/** Setup the XF forum identity **/ /** Setup the XF forum identity **/
fidentity.update_forum(); fidentity.update_forum();
@ -328,9 +330,9 @@ function main() {
if(_resize_timeout) if(_resize_timeout)
clearTimeout(_resize_timeout); clearTimeout(_resize_timeout);
_resize_timeout = setTimeout(() => { _resize_timeout = setTimeout(() => {
for(const connection of server_connections.server_connection_handlers()) for(const connection of server_connections.all_connections())
connection.invoke_resized_on_activate = true; connection.invoke_resized_on_activate = true;
const active_connection = server_connections.active_connection_handler(); const active_connection = server_connections.active_connection();
if(active_connection) if(active_connection)
active_connection.resize_elements(); active_connection.resize_elements();
$(".window-resize-listener").trigger('resize'); $(".window-resize-listener").trigger('resize');
@ -346,13 +348,13 @@ function main() {
log.info(LogCategory.STATISTICS, tr("Received user count update: %o"), status); log.info(LogCategory.STATISTICS, tr("Received user count update: %o"), status);
}); });
server_connections.set_active_connection_handler(server_connections.server_connection_handlers()[0]); server_connections.set_active_connection(server_connections.all_connections()[0]);
(window as any).test_upload = (message?: string) => { (window as any).test_upload = (message?: string) => {
message = message || "Hello World"; message = message || "Hello World";
const connection = server_connections.active_connection_handler(); const connection = server_connections.active_connection();
connection.fileManager.upload_file({ connection.fileManager.upload_file({
size: message.length, size: message.length,
overwrite: true, overwrite: true,
@ -376,7 +378,7 @@ function main() {
/* schedule it a bit later then the main because the main function is still within the loader */ /* schedule it a bit later then the main because the main function is still within the loader */
setTimeout(() => { setTimeout(() => {
const connection = server_connections.active_connection_handler(); const connection = server_connections.active_connection();
/* /*
Modals.createChannelModal(connection, undefined, undefined, connection.permissions, (cb, perms) => { Modals.createChannelModal(connection, undefined, undefined, connection.permissions, (cb, perms) => {
@ -512,7 +514,7 @@ const task_connect_handler: loader.Task = {
loader.register_task(loader.Stage.LOADED, { loader.register_task(loader.Stage.LOADED, {
priority: 0, priority: 0,
function: async () => handle_connect_request(connect_data, server_connections.active_connection_handler() || server_connections.spawn_server_connection_handler()), function: async () => handle_connect_request(connect_data, server_connections.active_connection() || server_connections.spawn_server_connection()),
name: tr("default url connect") name: tr("default url connect")
}); });
} }
@ -523,7 +525,7 @@ const task_connect_handler: loader.Task = {
}; };
chandler.callback_execute = data => { chandler.callback_execute = data => {
handle_connect_request(data, server_connections.spawn_server_connection_handler()); handle_connect_request(data, server_connections.spawn_server_connection());
return true; return true;
} }
} }

View File

@ -184,18 +184,34 @@ export class Settings extends StaticSettings {
description: 'Triggers a loading error at the end of the loading process.' description: 'Triggers a loading error at the end of the loading process.'
}; };
/* Control bar */ /* Default client states */
static readonly KEY_CONTROL_MUTE_INPUT: SettingsKey<boolean> = { static readonly KEY_CLIENT_STATE_MICROPHONE_MUTED: SettingsKey<boolean> = {
key: 'mute_input' key: 'client_state_microphone_muted',
default_value: false,
fallback_keys: ["mute_input"]
}; };
static readonly KEY_CONTROL_MUTE_OUTPUT: SettingsKey<boolean> = { static readonly KEY_CLIENT_STATE_SPEAKER_MUTED: SettingsKey<boolean> = {
key: 'mute_output' key: 'client_state_speaker_muted',
default_value: false,
fallback_keys: ["mute_output"]
}; };
static readonly KEY_CONTROL_SHOW_QUERIES: SettingsKey<boolean> = { static readonly KEY_CLIENT_STATE_QUERY_SHOWN: SettingsKey<boolean> = {
key: 'show_server_queries' key: 'client_state_query_shown',
default_value: false,
fallback_keys: ["show_server_queries"]
}; };
static readonly KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL: SettingsKey<boolean> = { static readonly KEY_CLIENT_STATE_SUBSCRIBE_ALL_CHANNELS: SettingsKey<boolean> = {
key: 'channel_subscribe_all' key: 'client_state_subscribe_all_channels',
default_value: true,
fallback_keys: ["channel_subscribe_all"]
};
static readonly KEY_CLIENT_STATE_AWAY: SettingsKey<boolean> = {
key: 'client_state_away',
default_value: false
};
static readonly KEY_CLIENT_AWAY_MESSAGE: SettingsKey<string> = {
key: 'client_away_message',
default_value: ""
}; };
/* Connect parameters */ /* Connect parameters */
@ -367,6 +383,8 @@ export class Settings extends StaticSettings {
static initialize() { static initialize() {
settings = new Settings(); settings = new Settings();
(window as any).settings = settings;
(window as any).Settings = Settings;
} }
private cacheGlobal = {}; private cacheGlobal = {};
@ -417,9 +435,7 @@ export class Settings extends StaticSettings {
changeGlobal<T>(key: string | SettingsKey<T>, value?: T){ changeGlobal<T>(key: string | SettingsKey<T>, value?: T){
key = Settings.keyify(key); key = Settings.keyify(key);
if(this.cacheGlobal[key.key] === value) return;
if(this.cacheGlobal[key.key] == value) return;
this.updated = true; this.updated = true;
this.cacheGlobal[key.key] = StaticSettings.transformOtS(value); this.cacheGlobal[key.key] = StaticSettings.transformOtS(value);

View File

@ -12,6 +12,8 @@ export abstract class ReactComponentBase<Properties, State> extends React.Compon
protected abstract default_state() : State; protected abstract default_state() : State;
updateState(updates: {[key in keyof State]?: State[key]}) { updateState(updates: {[key in keyof State]?: State[key]}) {
if(Object.keys(updates).findIndex(e => updates[e] !== this.state[e]) === -1)
return; /* no state has been changed */
this.setState(Object.assign(this.state, updates)); this.setState(Object.assign(this.state, updates));
} }

View File

@ -1,7 +1,7 @@
import {Icon, IconManager} from "tc-shared/FileManager"; import {Icon, IconManager} from "tc-shared/FileManager";
import {spawnBookmarkModal} from "tc-shared/ui/modal/ModalBookmarks"; import {spawnBookmarkModal} from "tc-shared/ui/modal/ModalBookmarks";
import { import {
add_current_server, add_server_to_bookmarks,
Bookmark, Bookmark,
bookmarks, bookmarks,
BookmarkType, BookmarkType,
@ -23,7 +23,6 @@ import {spawnAbout} from "tc-shared/ui/modal/ModalAbout";
import {server_connections} from "tc-shared/ui/frames/connection_handlers"; import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import * as loader from "tc-loader"; import * as loader from "tc-loader";
import {formatMessage} from "tc-shared/ui/frames/chat"; import {formatMessage} from "tc-shared/ui/frames/chat";
import * as slog from "tc-shared/ui/frames/server_log";
import {control_bar_instance} from "tc-shared/ui/frames/control-bar"; import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
export interface HRItem { } export interface HRItem { }
@ -268,7 +267,7 @@ export function rebuild_bookmarks() {
_items_bookmark.add_current = _items_bookmark.root.append_item(tr("Add current server to bookmarks")); _items_bookmark.add_current = _items_bookmark.root.append_item(tr("Add current server to bookmarks"));
_items_bookmark.add_current.icon('client-bookmark_add'); _items_bookmark.add_current.icon('client-bookmark_add');
_items_bookmark.add_current.click(() => add_current_server()); _items_bookmark.add_current.click(() => add_server_to_bookmarks(server_connections.active_connection()));
_state_updater["bookmarks.ac"] = { item: _items_bookmark.add_current, conditions: [condition_connected]}; _state_updater["bookmarks.ac"] = { item: _items_bookmark.add_current, conditions: [condition_connected]};
} }
@ -320,7 +319,7 @@ export function update_state() {
} }
const condition_connected = () => { const condition_connected = () => {
const scon = server_connections ? server_connections.active_connection_handler() : undefined; const scon = server_connections ? server_connections.active_connection() : undefined;
return scon && scon.connected; return scon && scon.connected;
}; };
@ -341,13 +340,8 @@ export function initialize() {
item.click(() => spawnConnectModal({})); item.click(() => spawnConnectModal({}));
const do_disconnect = (handlers: ConnectionHandler[]) => { const do_disconnect = (handlers: ConnectionHandler[]) => {
for(const handler of handlers) { for(const handler of handlers)
handler.cancel_reconnect(true); handler.disconnectFromServer();
handler.handleDisconnect(DisconnectReason.REQUESTED); //TODO message?
server_connections.active_connection_handler().serverConnection.disconnect();
handler.sound.play(Sound.CONNECTION_DISCONNECTED);
handler.log.log(slog.Type.DISCONNECTED, {});
}
control_bar_instance()?.events().fire("update_state", { state: "connect-state" }); control_bar_instance()?.events().fire("update_state", { state: "connect-state" });
update_state(); update_state();
@ -356,7 +350,7 @@ export function initialize() {
item.icon('client-disconnect'); item.icon('client-disconnect');
item.disabled(true); item.disabled(true);
item.click(() => { item.click(() => {
const handler = server_connections.active_connection_handler(); const handler = server_connections.active_connection();
do_disconnect([handler]); do_disconnect([handler]);
}); });
_state_updater["connection.dc"] = { item: item, conditions: [() => condition_connected()]}; _state_updater["connection.dc"] = { item: item, conditions: [() => condition_connected()]};
@ -364,10 +358,10 @@ export function initialize() {
item = menu.append_item(tr("Disconnect from all servers")); item = menu.append_item(tr("Disconnect from all servers"));
item.icon('client-disconnect'); item.icon('client-disconnect');
item.click(() => { item.click(() => {
do_disconnect(server_connections.server_connection_handlers()); do_disconnect(server_connections.all_connections());
}); });
_state_updater["connection.dca"] = { item: item, conditions: [], update_handler: (item) => { _state_updater["connection.dca"] = { item: item, conditions: [], update_handler: (item) => {
item.visible(server_connections && server_connections.server_connection_handlers().length > 1); item.visible(server_connections && server_connections.all_connections().length > 1);
return true; return true;
}}; }};
@ -394,35 +388,35 @@ export function initialize() {
item = menu.append_item(tr("Server Groups")); item = menu.append_item(tr("Server Groups"));
item.icon("client-permission_server_groups"); item.icon("client-permission_server_groups");
item.click(() => { item.click(() => {
spawnPermissionEdit(server_connections.active_connection_handler(), "sg").open(); spawnPermissionEdit(server_connections.active_connection(), "sg").open();
}); });
_state_updater["permission.sg"] = { item: item, conditions: [condition_connected]}; _state_updater["permission.sg"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Client Permissions")); item = menu.append_item(tr("Client Permissions"));
item.icon("client-permission_client"); item.icon("client-permission_client");
item.click(() => { item.click(() => {
spawnPermissionEdit(server_connections.active_connection_handler(), "clp").open(); spawnPermissionEdit(server_connections.active_connection(), "clp").open();
}); });
_state_updater["permission.clp"] = { item: item, conditions: [condition_connected]}; _state_updater["permission.clp"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Channel Client Permissions")); item = menu.append_item(tr("Channel Client Permissions"));
item.icon("client-permission_client"); item.icon("client-permission_client");
item.click(() => { item.click(() => {
spawnPermissionEdit(server_connections.active_connection_handler(), "clchp").open(); spawnPermissionEdit(server_connections.active_connection(), "clchp").open();
}); });
_state_updater["permission.chclp"] = { item: item, conditions: [condition_connected]}; _state_updater["permission.chclp"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Channel Groups")); item = menu.append_item(tr("Channel Groups"));
item.icon("client-permission_channel"); item.icon("client-permission_channel");
item.click(() => { item.click(() => {
spawnPermissionEdit(server_connections.active_connection_handler(), "cg").open(); spawnPermissionEdit(server_connections.active_connection(), "cg").open();
}); });
_state_updater["permission.cg"] = { item: item, conditions: [condition_connected]}; _state_updater["permission.cg"] = { item: item, conditions: [condition_connected]};
item = menu.append_item(tr("Channel Permissions")); item = menu.append_item(tr("Channel Permissions"));
item.icon("client-permission_channel"); item.icon("client-permission_channel");
item.click(() => { item.click(() => {
spawnPermissionEdit(server_connections.active_connection_handler(), "chp").open(); spawnPermissionEdit(server_connections.active_connection(), "chp").open();
}); });
_state_updater["permission.cp"] = { item: item, conditions: [condition_connected]}; _state_updater["permission.cp"] = { item: item, conditions: [condition_connected]};
@ -440,7 +434,7 @@ export function initialize() {
//TODO: Fixeme use one method for the control bar and here! //TODO: Fixeme use one method for the control bar and here!
createInputModal(tr("Use token"), tr("Please enter your token/privilege key"), message => message.length > 0, result => { createInputModal(tr("Use token"), tr("Please enter your token/privilege key"), message => message.length > 0, result => {
if(!result) return; if(!result) return;
const scon = server_connections.active_connection_handler(); const scon = server_connections.active_connection();
if(scon.serverConnection.connected) if(scon.serverConnection.connected)
scon.serverConnection.send_command("tokenuse", { scon.serverConnection.send_command("tokenuse", {
@ -476,7 +470,7 @@ export function initialize() {
item = menu.append_item(tr("Ban List")); item = menu.append_item(tr("Ban List"));
item.icon('client-ban_list'); item.icon('client-ban_list');
item.click(() => { item.click(() => {
const scon = server_connections.active_connection_handler(); const scon = server_connections.active_connection();
if(scon && scon.connected) { if(scon && scon.connected) {
if(scon.permissions.neededPermission(PermissionType.B_CLIENT_BAN_LIST).granted(1)) { if(scon.permissions.neededPermission(PermissionType.B_CLIENT_BAN_LIST).granted(1)) {
openBanList(scon); openBanList(scon);
@ -493,7 +487,7 @@ export function initialize() {
item = menu.append_item(tr("Query List")); item = menu.append_item(tr("Query List"));
item.icon('client-server_query'); item.icon('client-server_query');
item.click(() => { item.click(() => {
const scon = server_connections.active_connection_handler(); const scon = server_connections.active_connection();
if(scon && scon.connected) { if(scon && scon.connected) {
if(scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_LIST).granted(1) || scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_LIST_OWN).granted(1)) { if(scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_LIST).granted(1) || scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_LIST_OWN).granted(1)) {
spawnQueryManage(scon); spawnQueryManage(scon);
@ -510,7 +504,7 @@ export function initialize() {
item = menu.append_item(tr("Query Create")); item = menu.append_item(tr("Query Create"));
item.icon('client-server_query'); item.icon('client-server_query');
item.click(() => { item.click(() => {
const scon = server_connections.active_connection_handler(); const scon = server_connections.active_connection();
if(scon && scon.connected) { if(scon && scon.connected) {
if(scon.permissions.neededPermission(PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN).granted(1) || scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_CREATE).granted(1)) { if(scon.permissions.neededPermission(PermissionType.B_CLIENT_CREATE_MODIFY_SERVERQUERY_LOGIN).granted(1) || scon.permissions.neededPermission(PermissionType.B_CLIENT_QUERY_CREATE).granted(1)) {
spawnQueryCreate(scon); spawnQueryCreate(scon);

View File

@ -1,14 +1,15 @@
import {ConnectionHandler, DisconnectReason} from "tc-shared/ConnectionHandler"; import {ConnectionHandler, DisconnectReason} from "tc-shared/ConnectionHandler";
import {Settings, settings} from "tc-shared/settings"; import {Settings, settings} from "tc-shared/settings";
import * as top_menu from "./MenuBar"; import * as top_menu from "./MenuBar";
import {control_bar_instance} from "tc-shared/ui/frames/control-bar"; import {Registry} from "tc-shared/events";
import {client_control_events} from "tc-shared/main";
export let server_connections: ServerConnectionManager; export let server_connections: ConnectionManager;
export function initialize(manager: ServerConnectionManager) { export function initialize() {
server_connections = manager; if(server_connections) throw tr("Connection manager has already been initialized");
server_connections = new ConnectionManager($("#connection-handlers"));
} }
export class ServerConnectionManager { export class ConnectionManager {
private readonly event_registry: Registry<ConnectionManagerEvents>;
private connection_handlers: ConnectionHandler[] = []; private connection_handlers: ConnectionHandler[] = [];
private active_handler: ConnectionHandler | undefined; private active_handler: ConnectionHandler | undefined;
@ -23,7 +24,20 @@ export class ServerConnectionManager {
private _tag_button_scoll_right: JQuery; private _tag_button_scoll_right: JQuery;
private _tag_button_scoll_left: JQuery; private _tag_button_scoll_left: JQuery;
private default_server_state: {
microphone_disabled: boolean,
speaker_disabled: boolean,
away: string | boolean
} = {
away: false,
speaker_disabled: false,
microphone_disabled: false
};
constructor(tag: JQuery) { constructor(tag: JQuery) {
this.event_registry = new Registry<ConnectionManagerEvents>();
this.event_registry.enable_debug("connection-manager");
this._tag = tag; this._tag = tag;
if(settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION, false)) if(settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION, false))
@ -42,23 +56,42 @@ export class ServerConnectionManager {
this._container_hostbanner = $("#hostbanner"); this._container_hostbanner = $("#hostbanner");
this._container_chat = $("#chat"); this._container_chat = $("#chat");
this.set_active_connection_handler(undefined); this.set_active_connection(undefined);
} }
spawn_server_connection_handler() : ConnectionHandler { events() : Registry<ConnectionManagerEvents> {
return this.event_registry;
}
spawn_server_connection() : ConnectionHandler {
const handler = new ConnectionHandler(); const handler = new ConnectionHandler();
handler.initialize_client_state(this.active_handler);
this.connection_handlers.push(handler); this.connection_handlers.push(handler);
control_bar.update_button_away();
control_bar.initialize_connection_handler_state(handler); //FIXME: Load last status from last connection or via global variables!
/*
handler.set_away_status(this.default_server_state.away, false);
handler.client_status.input_muted = this.default_server_state.microphone_disabled;
handler.client_status.output_muted = this.default_server_state.speaker_disabled;
if(!this.default_server_state.microphone_disabled)
handler.acquire_recorder(default_recorder, true);
*/
handler.tag_connection_handler.appendTo(this._tag_connection_entries); handler.tag_connection_handler.appendTo(this._tag_connection_entries);
this._tag.toggleClass("shown", this.connection_handlers.length > 1); this._tag.toggleClass("shown", this.connection_handlers.length > 1);
this._update_scroll(); this._update_scroll();
this.event_registry.fire("notify_handler_created", { handler: handler });
return handler; return handler;
} }
destroy_server_connection_handler(handler: ConnectionHandler) { destroy_server_connection(handler: ConnectionHandler) {
this.connection_handlers.remove(handler); if(this.connection_handlers.length <= 1)
throw "cannot deleted the last connection handler";
if(!this.connection_handlers.remove(handler))
throw "unknown connection handler";
handler.tag_connection_handler.remove(); handler.tag_connection_handler.remove();
this._update_scroll(); this._update_scroll();
this._tag.toggleClass("shown", this.connection_handlers.length > 1); this._tag.toggleClass("shown", this.connection_handlers.length > 1);
@ -70,16 +103,22 @@ export class ServerConnectionManager {
} }
if(handler === this.active_handler) if(handler === this.active_handler)
this.set_active_connection_handler(this.connection_handlers[0]); this.set_active_connection_(this.connection_handlers[0]);
this.event_registry.fire("notify_handler_deleted", { handler: handler });
/* destroy all elements */ /* destroy all elements */
handler.destroy(); handler.destroy();
} }
set_active_connection_handler(handler: ConnectionHandler) { set_active_connection(handler: ConnectionHandler) {
if(handler && this.connection_handlers.indexOf(handler) == -1) if(handler && this.connection_handlers.indexOf(handler) == -1)
throw "Handler hasn't been registered or is already obsolete!"; throw "Handler hasn't been registered or is already obsolete!";
if(handler === this.active_handler)
return;
this.set_active_connection_(handler);
}
private set_active_connection_(handler: ConnectionHandler) {
this._tag_connection_entries.find(".active").removeClass("active"); this._tag_connection_entries.find(".active").removeClass("active");
this._container_channel_tree.children().detach(); this._container_channel_tree.children().detach();
this._container_chat.children().detach(); this._container_chat.children().detach();
@ -97,16 +136,20 @@ export class ServerConnectionManager {
if(handler.invoke_resized_on_activate) if(handler.invoke_resized_on_activate)
handler.resize_elements(); handler.resize_elements();
} }
const old_handler = this.active_handler;
this.active_handler = handler; this.active_handler = handler;
client_control_events.fire("action_set_active_connection_handler", { handler: handler }); //FIXME: This even should set the new handler, not vice versa! this.event_registry.fire("notify_active_handler_changed", {
top_menu.update_state(); old_handler: old_handler,
new_handler: handler
});
top_menu.update_state(); //FIXME: Top menu should listen to our events!
} }
active_connection_handler() : ConnectionHandler | undefined { active_connection() : ConnectionHandler | undefined {
return this.active_handler; return this.active_handler;
} }
server_connection_handlers() : ConnectionHandler[] { all_connections() : ConnectionHandler[] {
return this.connection_handlers; return this.connection_handlers;
} }
@ -141,3 +184,21 @@ export class ServerConnectionManager {
this._tag_button_scoll_right.toggleClass("disabled", scroll + this._tag_connection_entries.width() + 2 >= this._tag_connection_entries[0].scrollWidth); this._tag_button_scoll_right.toggleClass("disabled", scroll + this._tag_connection_entries.width() + 2 >= this._tag_connection_entries[0].scrollWidth);
} }
} }
export interface ConnectionManagerEvents {
notify_handler_created: {
handler: ConnectionHandler
},
/* This will also trigger when a connection gets deleted. So if you're just interested to connect event handler to the active connection,
unregister them from the old handler and register them for the new handler every time */
notify_active_handler_changed: {
old_handler: ConnectionHandler | undefined,
new_handler: ConnectionHandler | undefined
},
/* Will never fire on an active connection handler! */
notify_handler_deleted: {
handler: ConnectionHandler
}
}

View File

@ -1,30 +0,0 @@
import {Registry} from "tc-shared/events";
import {ControlBarEvents} from "tc-shared/ui/frames/control-bar/index";
import {manager, Sound} from "tc-shared/sound/Sounds";
function initialize_sounds(event_registry: Registry<ControlBarEvents>) {
{
let microphone_muted = undefined;
event_registry.on("update_microphone_state", event => {
if(microphone_muted === event.muted) return;
if(typeof microphone_muted !== "undefined")
manager.play(event.muted ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED);
microphone_muted = event.muted;
})
}
{
let speakers_muted = undefined;
event_registry.on("update_speaker_state", event => {
if(speakers_muted === event.muted) return;
if(typeof speakers_muted !== "undefined")
manager.play(event.muted ? Sound.SOUND_MUTED : Sound.SOUND_ACTIVATED);
speakers_muted = event.muted;
})
}
}
export = (event_registry: Registry<ControlBarEvents>) => {
initialize_sounds(event_registry);
};
//TODO: Left action handler!

View File

@ -1,39 +1,54 @@
@import "../../../../css/static/properties"; @import "../../../../css/static/properties";
@import "../../../../css/static/mixin"; @import "../../../../css/static/mixin";
$border_color_activated: rgba(255, 255, 255, .75); /* Variables */
html:root {
--menu-bar-button-background: #454545;
--menu-bar-button-background-hover: #393c43;
--menu-bar-button-background-activated: #2f3841;
--menu-bar-button-background-activated-red: #412f2f;
--menu-bar-button-background-activated-hover: #263340;
--menu-bar-button-background-activated-red-hover: #402626;
--menu-bar-button-border: #454545;
--menu-bar-button-border-hover: #4a4c55;
--menu-bar-button-border-activated: #005fa1;
--menu-bar-button-border-activated-red: #a10000;
--menu-bar-button-border-activated-hover: #005fa1;
--menu-bar-button-border-activated-red-hover: #a10000;
}
/* border etc */ /* border etc */
.button, .dropdownArrow { .button, .dropdownArrow {
text-align: center; text-align: center;
border: .05em solid rgba(0, 0, 0, 0); border: .05em solid var(--menu-bar-button-border);
border-radius: $border_radius_small; border-radius: $border_radius_small;
background-color: #454545; background-color: var(--menu-bar-button-background);
&:hover { &:hover {
background-color: #393c43; background-color: var(--menu-bar-button-background-hover);
border-color: #4a4c55; border-color: var(--menu-bar-button-border-hover);
/*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/ /* box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19); */
} }
&.activated { &.activated {
background-color: #2f3841; background-color: var(--menu-bar-button-background-activated);
border-color: #005fa1; border-color: var(--menu-bar-button-border-activated);
&:hover { &:hover {
background-color: #263340; background-color: var(--menu-bar-button-background-activated-hover);
border-color: #005fa1; border-color: var(--menu-bar-button-border-activated-hover);
} }
&.theme-red { &.theme-red {
background-color: #412f2f; background-color: var(--menu-bar-button-background-activated-red);
border-color: #a10000; border-color: var(--menu-bar-button-border-activated-red);
&:hover { &:hover {
background-color: #402626; background-color: var(--menu-bar-button-background-activated-red-hover);
border-color: #a10000; border-color: var(--menu-bar-button-border-activated-red-hover);
} }
} }
} }

View File

@ -78,7 +78,7 @@ export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
} }
private onClick() { private onClick() {
const new_state = !this.state.switched; const new_state = !(this.state.switched || this.props.switched);
const result = this.props.onToggle?.call(undefined, new_state); const result = this.props.onToggle?.call(undefined, new_state);
if(this.props.autoSwitch) if(this.props.autoSwitch)
this.updateState({ switched: typeof result === "boolean" ? result : new_state }); this.updateState({ switched: typeof result === "boolean" ? result : new_state });

View File

@ -1,6 +1,11 @@
@import "../../../../css/static/properties"; @import "../../../../css/static/properties";
@import "../../../../css/static/mixin"; @import "../../../../css/static/mixin";
/* Variables */
html:root {
--menu-bar-background: #454545;
}
/* max height is 2em */ /* max height is 2em */
.controlBar { .controlBar {
display: flex; display: flex;
@ -10,6 +15,7 @@
height: 100%; height: 100%;
align-items: center; align-items: center;
background: var(--menu-bar-background);
/* tmp fix for ultra small devices */ /* tmp fix for ultra small devices */
overflow-y: visible; overflow-y: visible;

View File

@ -3,11 +3,12 @@ import {Button} from "./button";
import {DropdownEntry} from "tc-shared/ui/frames/control-bar/dropdown"; import {DropdownEntry} from "tc-shared/ui/frames/control-bar/dropdown";
import {Translatable} from "tc-shared/ui/elements/i18n"; import {Translatable} from "tc-shared/ui/elements/i18n";
import {ReactComponentBase} from "tc-shared/ui/elements/ReactComponentBase"; import {ReactComponentBase} from "tc-shared/ui/elements/ReactComponentBase";
import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import {ConnectionEvents, ConnectionHandler, ConnectionStateUpdateType} from "tc-shared/ConnectionHandler";
import {Event, EventHandler, ReactEventHandler, Registry} from "tc-shared/events"; import {Event, EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
import {server_connections} from "tc-shared/ui/frames/connection_handlers"; import {ConnectionManagerEvents, server_connections} from "tc-shared/ui/frames/connection_handlers";
import {Settings, settings} from "tc-shared/settings"; import {Settings, settings} from "tc-shared/settings";
import { import {
add_server_to_bookmarks,
Bookmark, Bookmark,
bookmarks, bookmarks,
BookmarkType, BookmarkType,
@ -17,8 +18,9 @@ import {
} from "tc-shared/bookmarks"; } from "tc-shared/bookmarks";
import {IconManager} from "tc-shared/FileManager"; import {IconManager} from "tc-shared/FileManager";
import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import {client_control_events} from "tc-shared/main"; import {createInputModal} from "tc-shared/ui/elements/Modal";
const register_actions = require("./actions"); import {default_recorder} from "tc-shared/voice/RecorderProfile";
import {global_client_actions} from "tc-shared/events/GlobalEvents";
const cssStyle = require("./index.scss"); const cssStyle = require("./index.scss");
const cssButtonStyle = require("./button.scss"); const cssButtonStyle = require("./button.scss");
@ -29,7 +31,7 @@ export interface ConnectionState {
} }
@ReactEventHandler(obj => obj.props.event_registry) @ReactEventHandler(obj => obj.props.event_registry)
class ConnectButton extends ReactComponentBase<{ multiSession: boolean; event_registry: Registry<ControlBarEvents> }, ConnectionState> { class ConnectButton extends ReactComponentBase<{ multiSession: boolean; event_registry: Registry<InternalControlBarEvents> }, ConnectionState> {
protected default_state(): ConnectionState { protected default_state(): ConnectionState {
return { return {
connected: false, connected: false,
@ -43,51 +45,51 @@ class ConnectButton extends ReactComponentBase<{ multiSession: boolean; event_re
if(!this.state.connected) { if(!this.state.connected) {
subentries.push( subentries.push(
<DropdownEntry key={"connect-server"} icon={"client-connect"} text={<Translatable message={"Connect to a server"} />} <DropdownEntry key={"connect-server"} icon={"client-connect"} text={<Translatable message={"Connect to a server"} />}
onClick={ () => client_control_events.fire("action_open_connect", { new_tab: false }) } /> onClick={ () => global_client_actions.fire("action_open_window_connect", {new_tab: false }) } />
); );
} else { } else {
subentries.push( subentries.push(
<DropdownEntry key={"disconnect-current"} icon={"client-disconnect"} text={<Translatable message={"Disconnect from current server"} />} <DropdownEntry key={"disconnect-current-a"} icon={"client-disconnect"} text={<Translatable message={"Disconnect from current server"} />}
onClick={ () => client_control_events.fire("action_disconnect", { globally: false }) }/> onClick={ () => this.props.event_registry.fire("action_disconnect", { globally: false }) }/>
); );
} }
if(this.state.connectedAnywhere) { if(this.state.connectedAnywhere) {
subentries.push( subentries.push(
<DropdownEntry key={"disconnect-current"} icon={"client-disconnect"} text={<Translatable message={"Disconnect from all servers"} />} <DropdownEntry key={"disconnect-current-b"} icon={"client-disconnect"} text={<Translatable message={"Disconnect from all servers"} />}
onClick={ () => client_control_events.fire("action_disconnect", { globally: true }) }/> onClick={ () => this.props.event_registry.fire("action_disconnect", { globally: true }) }/>
); );
} }
subentries.push( subentries.push(
<DropdownEntry key={"connect-new-tab"} icon={"client-connect"} text={<Translatable message={"Connect to a server in another tab"} />} <DropdownEntry key={"connect-new-tab"} icon={"client-connect"} text={<Translatable message={"Connect to a server in another tab"} />}
onClick={ () => client_control_events.fire("action_open_connect", { new_tab: true }) } /> onClick={ () => global_client_actions.fire("action_open_window_connect", { new_tab: true }) } />
); );
} }
if(!this.state.connected) { if(!this.state.connected) {
return ( return (
<Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-connect"} tooltip={tr("Connect to a server")} <Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-connect"} tooltip={tr("Connect to a server")}
onToggle={ () => client_control_events.fire("action_open_connect", { new_tab: false }) }> onToggle={ () => global_client_actions.fire("action_open_window_connect", { new_tab: false }) }>
{subentries} {subentries}
</Button> </Button>
); );
} else { } else {
return ( return (
<Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-disconnect"} tooltip={tr("Disconnect from server")} <Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-disconnect"} tooltip={tr("Disconnect from server")}
onToggle={ () => client_control_events.fire("action_disconnect", { globally: false }) }> onToggle={ () => this.props.event_registry.fire("action_disconnect", { globally: false }) }>
{subentries} {subentries}
</Button> </Button>
); );
} }
} }
@EventHandler<ControlBarEvents>("update_connect_state") @EventHandler<InternalControlBarEvents>("update_connect_state")
private handleStateUpdate(state: ConnectionState) { private handleStateUpdate(state: ConnectionState) {
this.updateState(state); this.updateState(state);
} }
} }
@ReactEventHandler(obj => obj.props.event_registry) @ReactEventHandler(obj => obj.props.event_registry)
class BookmarkButton extends ReactComponentBase<{ event_registry: Registry<ControlBarEvents> }, {}> { class BookmarkButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, {}> {
private button_ref: React.RefObject<Button>; private button_ref: React.RefObject<Button>;
protected initialize() { protected initialize() {
@ -105,8 +107,9 @@ class BookmarkButton extends ReactComponentBase<{ event_registry: Registry<Contr
return ( return (
<Button ref={this.button_ref} dropdownButtonExtraClass={cssButtonStyle.buttonBookmarks} autoSwitch={false} iconNormal={"client-bookmark_manager"}> <Button ref={this.button_ref} dropdownButtonExtraClass={cssButtonStyle.buttonBookmarks} autoSwitch={false} iconNormal={"client-bookmark_manager"}>
<DropdownEntry icon={"client-bookmark_manager"} text={<Translatable message={"Manage bookmarks"} />} <DropdownEntry icon={"client-bookmark_manager"} text={<Translatable message={"Manage bookmarks"} />}
onClick={() => client_control_events.fire("action_open_window", { window: "bookmark-manage" })} /> onClick={() => this.props.event_registry.fire("action_open_window", { window: "bookmark-manage" })} />
<DropdownEntry icon={"client-bookmark_add"} text={<Translatable message={"Add current server to bookmarks"} />} /> <DropdownEntry icon={"client-bookmark_add"} text={<Translatable message={"Add current server to bookmarks"} />}
onClick={() => this.props.event_registry.fire("action_add_current_server_to_bookmarks")} />
{marks} {marks}
</Button> </Button>
) )
@ -160,7 +163,7 @@ class BookmarkButton extends ReactComponentBase<{ event_registry: Registry<Contr
})); }));
} }
@EventHandler<ControlBarEvents>("update_bookmarks") @EventHandler<InternalControlBarEvents>("update_bookmarks")
private handleStateUpdate() { private handleStateUpdate() {
this.forceUpdate(); this.forceUpdate();
} }
@ -173,7 +176,7 @@ export interface AwayState {
} }
@ReactEventHandler(obj => obj.props.event_registry) @ReactEventHandler(obj => obj.props.event_registry)
class AwayButton extends ReactComponentBase<{ event_registry: Registry<ControlBarEvents> }, AwayState> { class AwayButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, AwayState> {
protected default_state(): AwayState { protected default_state(): AwayState {
return { return {
away: false, away: false,
@ -186,35 +189,42 @@ class AwayButton extends ReactComponentBase<{ event_registry: Registry<ControlBa
let dropdowns = []; let dropdowns = [];
if(this.state.away) { if(this.state.away) {
dropdowns.push(<DropdownEntry key={"cgo"} icon={"client-present"} text={<Translatable message={"Go online"} />} dropdowns.push(<DropdownEntry key={"cgo"} icon={"client-present"} text={<Translatable message={"Go online"} />}
onClick={() => client_control_events.fire("action_disable_away", { globally: false })} />); onClick={() => this.props.event_registry.fire("action_disable_away", { globally: false })} />);
} else { } else {
dropdowns.push(<DropdownEntry key={"sas"} icon={"client-away"} text={<Translatable message={"Set away on this server"} />} dropdowns.push(<DropdownEntry key={"sas"} icon={"client-away"} text={<Translatable message={"Set away on this server"} />}
onClick={() => client_control_events.fire("action_set_away", { globally: false, prompt_reason: false })} />); onClick={() => this.props.event_registry.fire("action_set_away", { globally: false, prompt_reason: false })} />);
} }
dropdowns.push(<DropdownEntry key={"sam"} icon={"client-away"} text={<Translatable message={"Set away message on this server"} />} dropdowns.push(<DropdownEntry key={"sam"} icon={"client-away"} text={<Translatable message={"Set away message on this server"} />}
onClick={() => client_control_events.fire("action_set_away", { globally: false, prompt_reason: true })} />); onClick={() => this.props.event_registry.fire("action_set_away", { globally: false, prompt_reason: true })} />);
dropdowns.push(<hr key={"-hr"} />); dropdowns.push(<hr key={"-hr"} />);
if(this.state.awayAnywhere) { if(this.state.awayAnywhere) {
dropdowns.push(<DropdownEntry key={"goa"} icon={"client-present"} text={<Translatable message={"Go online for all servers"} />} dropdowns.push(<DropdownEntry key={"goa"} icon={"client-present"} text={<Translatable message={"Go online for all servers"} />}
onClick={() => client_control_events.fire("action_disable_away", { globally: true })} />); onClick={() => this.props.event_registry.fire("action_disable_away", { globally: true })} />);
} }
if(!this.state.awayAll) { if(!this.state.awayAll) {
dropdowns.push(<DropdownEntry key={"saa"} icon={"client-away"} text={<Translatable message={"Set away on all servers"} />} dropdowns.push(<DropdownEntry key={"saa"} icon={"client-away"} text={<Translatable message={"Set away on all servers"} />}
onClick={() => client_control_events.fire("action_set_away", { globally: true, prompt_reason: false })} />); onClick={() => this.props.event_registry.fire("action_set_away", { globally: true, prompt_reason: false })} />);
} }
dropdowns.push(<DropdownEntry key={"sama"} icon={"client-away"} text={<Translatable message={"Set away message for all servers"} />} dropdowns.push(<DropdownEntry key={"sama"} icon={"client-away"} text={<Translatable message={"Set away message for all servers"} />}
onClick={() => client_control_events.fire("action_set_away", { globally: true, prompt_reason: true })} />); onClick={() => this.props.event_registry.fire("action_set_away", { globally: true, prompt_reason: true })} />);
/* switchable because we're switching it manually */ /* switchable because we're switching it manually */
return ( return (
<Button autoSwitch={false} iconNormal={this.state.away ? "client-present" : "client-away"}> <Button autoSwitch={false} switched={this.state.away} iconNormal={this.state.away ? "client-present" : "client-away"} onToggle={this.handleButtonToggled.bind(this)}>
{dropdowns} {dropdowns}
</Button> </Button>
); );
} }
@EventHandler<ControlBarEvents>("update_away_state") private handleButtonToggled(state: boolean) {
if(state)
this.props.event_registry.fire("action_set_away", { globally: false, prompt_reason: false });
else
this.props.event_registry.fire("action_disable_away");
}
@EventHandler<InternalControlBarEvents>("update_away_state")
private handleStateUpdate(state: AwayState) { private handleStateUpdate(state: AwayState) {
this.updateState(state); this.updateState(state);
} }
@ -225,17 +235,17 @@ export interface ChannelSubscribeState {
} }
@ReactEventHandler(obj => obj.props.event_registry) @ReactEventHandler(obj => obj.props.event_registry)
class ChannelSubscribeButton extends ReactComponentBase<{ event_registry: Registry<ControlBarEvents> }, ChannelSubscribeState> { class ChannelSubscribeButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, ChannelSubscribeState> {
protected default_state(): ChannelSubscribeState { protected default_state(): ChannelSubscribeState {
return { subscribeEnabled: false }; return { subscribeEnabled: false };
} }
render() { render() {
return <Button switched={this.state.subscribeEnabled} autoSwitch={false} iconNormal={"client-unsubscribe_from_all_channels"} iconSwitched={"client-subscribe_to_all_channels"} return <Button switched={this.state.subscribeEnabled} autoSwitch={false} iconNormal={"client-unsubscribe_from_all_channels"} iconSwitched={"client-subscribe_to_all_channels"}
onToggle={flag => client_control_events.fire("action_set_channel_subscribe_mode", { subscribe: flag })}/>; onToggle={flag => this.props.event_registry.fire("action_set_subscribe", { subscribe: flag })}/>;
} }
@EventHandler<ControlBarEvents>("update_subscribe_state") @EventHandler<InternalControlBarEvents>("update_subscribe_state")
private handleStateUpdate(state: ChannelSubscribeState) { private handleStateUpdate(state: ChannelSubscribeState) {
this.updateState(state); this.updateState(state);
} }
@ -247,7 +257,7 @@ export interface MicrophoneState {
} }
@ReactEventHandler(obj => obj.props.event_registry) @ReactEventHandler(obj => obj.props.event_registry)
class MicrophoneButton extends ReactComponentBase<{ event_registry: Registry<ControlBarEvents> }, MicrophoneState> { class MicrophoneButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, MicrophoneState> {
protected default_state(): MicrophoneState { protected default_state(): MicrophoneState {
return { return {
enabled: false, enabled: false,
@ -258,15 +268,15 @@ class MicrophoneButton extends ReactComponentBase<{ event_registry: Registry<Con
render() { render() {
if(!this.state.enabled) if(!this.state.enabled)
return <Button autoSwitch={false} iconNormal={"client-activate_microphone"} tooltip={tr("Enable your microphone on this server")} return <Button autoSwitch={false} iconNormal={"client-activate_microphone"} tooltip={tr("Enable your microphone on this server")}
onToggle={() => client_control_events.fire("action_toggle_microphone", { state: true })} />; onToggle={() => this.props.event_registry.fire("action_enable_microphone")} />;
if(this.state.muted) if(this.state.muted)
return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={"client-input_muted"} tooltip={tr("Unmute microphone")} return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={"client-input_muted"} tooltip={tr("Unmute microphone")}
onToggle={() => client_control_events.fire("action_toggle_microphone", { state: true })} />; onToggle={() => this.props.event_registry.fire("action_enable_microphone")} />;
return <Button colorTheme={"red"} autoSwitch={false} iconNormal={"client-input_muted"} tooltip={tr("Mute microphone")} return <Button colorTheme={"red"} autoSwitch={false} iconNormal={"client-input_muted"} tooltip={tr("Mute microphone")}
onToggle={() => client_control_events.fire("action_toggle_microphone", { state: false })} />; onToggle={() => this.props.event_registry.fire("action_disable_microphone")} />;
} }
@EventHandler<ControlBarEvents>("update_microphone_state") @EventHandler<InternalControlBarEvents>("update_microphone_state")
private handleStateUpdate(state: MicrophoneState) { private handleStateUpdate(state: MicrophoneState) {
this.updateState(state); this.updateState(state);
} }
@ -277,7 +287,7 @@ export interface SpeakerState {
} }
@ReactEventHandler(obj => obj.props.event_registry) @ReactEventHandler(obj => obj.props.event_registry)
class SpeakerButton extends ReactComponentBase<{ event_registry: Registry<ControlBarEvents> }, SpeakerState> { class SpeakerButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, SpeakerState> {
protected default_state(): SpeakerState { protected default_state(): SpeakerState {
return { return {
muted: false muted: false
@ -287,12 +297,12 @@ class SpeakerButton extends ReactComponentBase<{ event_registry: Registry<Contro
render() { render() {
if(this.state.muted) if(this.state.muted)
return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={"client-output_muted"} tooltip={tr("Unmute headphones")} return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={"client-output_muted"} tooltip={tr("Unmute headphones")}
onToggle={() => client_control_events.fire("action_toggle_speaker", { state: true })}/>; onToggle={() => this.props.event_registry.fire("action_enable_speaker")}/>;
return <Button colorTheme={"red"} autoSwitch={false} iconNormal={"client-output_muted"} tooltip={tr("Mute headphones")} return <Button colorTheme={"red"} autoSwitch={false} iconNormal={"client-output_muted"} tooltip={tr("Mute headphones")}
onToggle={() => client_control_events.fire("action_toggle_speaker", { state: false })}/>; onToggle={() => this.props.event_registry.fire("action_disable_speaker")}/>;
} }
@EventHandler<ControlBarEvents>("update_speaker_state") @EventHandler<InternalControlBarEvents>("update_speaker_state")
private handleStateUpdate(state: SpeakerState) { private handleStateUpdate(state: SpeakerState) {
this.updateState(state); this.updateState(state);
} }
@ -303,7 +313,7 @@ export interface QueryState {
} }
@ReactEventHandler(obj => obj.props.event_registry) @ReactEventHandler(obj => obj.props.event_registry)
class QueryButton extends ReactComponentBase<{ event_registry: Registry<ControlBarEvents> }, QueryState> { class QueryButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, QueryState> {
protected default_state() { protected default_state() {
return { return {
queryShown: false queryShown: false
@ -313,22 +323,22 @@ class QueryButton extends ReactComponentBase<{ event_registry: Registry<ControlB
render() { render() {
let toggle; let toggle;
if(this.state.queryShown) if(this.state.queryShown)
toggle = <DropdownEntry icon={""} text={<Translatable message={"Hide server queries"} />} toggle = <DropdownEntry icon={"client-toggle_server_query_clients"} text={<Translatable message={"Hide server queries"} />}
onClick={() => client_control_events.fire("action_toggle_query", { shown: false })}/>; onClick={() => this.props.event_registry.fire("action_toggle_query", { shown: false })}/>;
else else
toggle = <DropdownEntry icon={"client-toggle_server_query_clients"} text={<Translatable message={"Show server queries"} />} toggle = <DropdownEntry icon={"client-toggle_server_query_clients"} text={<Translatable message={"Show server queries"} />}
onClick={() => client_control_events.fire("action_toggle_query", { shown: true })}/>; onClick={() => this.props.event_registry.fire("action_toggle_query", { shown: true })}/>;
return ( return (
<Button switched={this.state.queryShown} autoSwitch={false} iconNormal={"client-server_query"} <Button switched={this.state.queryShown} autoSwitch={false} iconNormal={"client-server_query"}
onToggle={flag => client_control_events.fire("action_toggle_query", { shown: flag })}> onToggle={flag => this.props.event_registry.fire("action_toggle_query", { shown: flag })}>
{toggle} {toggle}
<DropdownEntry icon={"client-server_query"} text={<Translatable message={"Manage server queries"} />} <DropdownEntry icon={"client-server_query"} text={<Translatable message={"Manage server queries"} />}
onClick={() => client_control_events.fire("action_open_window", { window: "query-manage" })}/> onClick={() => this.props.event_registry.fire("action_open_window", { window: "query-manage" })}/>
</Button> </Button>
) )
} }
@EventHandler<ControlBarEvents>("update_query_state") @EventHandler<InternalControlBarEvents>("update_query_state")
private handleStateUpdate(state: QueryState) { private handleStateUpdate(state: QueryState) {
this.updateState(state); this.updateState(state);
} }
@ -341,7 +351,7 @@ export interface HostButtonState {
} }
@ReactEventHandler(obj => obj.props.event_registry) @ReactEventHandler(obj => obj.props.event_registry)
class HostButton extends ReactComponentBase<{ event_registry: Registry<ControlBarEvents> }, HostButtonState> { class HostButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, HostButtonState> {
protected default_state() { protected default_state() {
return { return {
url: undefined, url: undefined,
@ -370,7 +380,7 @@ class HostButton extends ReactComponentBase<{ event_registry: Registry<ControlBa
event.preventDefault(); event.preventDefault();
} }
@EventHandler<ControlBarEvents>("update_host_button") @EventHandler<InternalControlBarEvents>("update_host_button")
private handleStateUpdate(state: HostButtonState) { private handleStateUpdate(state: HostButtonState) {
this.updateState(state); this.updateState(state);
} }
@ -382,33 +392,25 @@ export interface ControlBarProperties {
@ReactEventHandler<ControlBar>(obj => obj.event_registry) @ReactEventHandler<ControlBar>(obj => obj.event_registry)
export class ControlBar extends React.Component<ControlBarProperties, {}> { export class ControlBar extends React.Component<ControlBarProperties, {}> {
private readonly event_registry: Registry<ControlBarEvents>; private readonly event_registry: Registry<InternalControlBarEvents>;
private connection: ConnectionHandler; private connection: ConnectionHandler;
private connection_handler_callbacks = {
notify_state_updated: this.handleConnectionHandlerStateChange.bind(this),
notify_connection_state_changed: this.handleConnectionHandlerConnectionStateChange.bind(this)
};
private connection_manager_callbacks = {
active_handler_changed: this.handleActiveConnectionHandlerChanged.bind(this)
};
constructor(props) { constructor(props) {
super(props); super(props);
this.event_registry = new Registry<ControlBarEvents>(); this.event_registry = new Registry<InternalControlBarEvents>();
this.event_registry.enable_debug("control-bar"); this.event_registry.enable_debug("control-bar");
register_actions(this.event_registry); initialize(this.event_registry);
} }
componentDidMount(): void { events() : Registry<InternalControlBarEvents> { return this.event_registry; }
}
/*
initialize_connection_handler_state(handler?: ConnectionHandler) {
handler.client_status.output_muted = this._button_speakers === "muted";
handler.client_status.input_muted = this._button_microphone === "muted";
handler.client_status.channel_subscribe_all = this._button_subscribe_all;
handler.client_status.queries_visible = this._button_query_visible;
}
*/
events() : Registry<ControlBarEvents> { return this.event_registry; }
render() { render() {
return ( return (
@ -428,22 +430,66 @@ export class ControlBar extends React.Component<ControlBarProperties, {}> {
) )
} }
@EventHandler<ControlBarEvents>("set_connection_handler") private handleActiveConnectionHandlerChanged(event: ConnectionManagerEvents["notify_active_handler_changed"]) {
private handleSetConnectionHandler(event: ControlBarEvents["set_connection_handler"]) { if(event.old_handler)
if(this.connection == event.handler) return; this.unregisterConnectionHandlerEvents(event.old_handler);
this.connection = event.handler; this.connection = event.new_handler;
if(event.new_handler)
this.registerConnectionHandlerEvents(event.new_handler);
this.event_registry.fire("set_connection_handler", { handler: this.connection });
this.event_registry.fire("update_state_all"); this.event_registry.fire("update_state_all");
} }
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"]) private unregisterConnectionHandlerEvents(target: ConnectionHandler) {
private updateStateHostButton(event: Event<ControlBarEvents>) { const events = target.events();
if(event.type === "update_state")4 events.off("notify_state_updated", this.connection_handler_callbacks.notify_state_updated);
if(event.as<"update_state">().state !== "host-button") events.off("notify_connection_state_changed", this.connection_handler_callbacks.notify_connection_state_changed);
}
private registerConnectionHandlerEvents(target: ConnectionHandler) {
const events = target.events();
events.on("notify_state_updated", this.connection_handler_callbacks.notify_state_updated);
events.on("notify_connection_state_changed", this.connection_handler_callbacks.notify_connection_state_changed);
}
componentDidMount(): void {
console.error(server_connections.events());
server_connections.events().on("notify_active_handler_changed", this.connection_manager_callbacks.active_handler_changed);
this.event_registry.fire("set_connection_handler", { handler: server_connections.active_connection() });
}
componentWillUnmount(): void {
server_connections.events().off("notify_active_handler_changed", this.connection_manager_callbacks.active_handler_changed);
}
/* Active server connection handler events */
private handleConnectionHandlerStateChange(event: ConnectionEvents["notify_state_updated"]) {
const type_mapping: {[T in ConnectionStateUpdateType]:ControlStateUpdateType[]} = {
"microphone": ["microphone"],
"speaker": ["speaker"],
"away": ["away"],
"subscribe": ["subscribe-mode"],
"query": ["query"]
};
for(const type of type_mapping[event.state] || [])
this.event_registry.fire("update_state", { state: type });
}
private handleConnectionHandlerConnectionStateChange(/* event: ConnectionEvents["notify_connection_state_changed"] */) {
this.event_registry.fire("update_state", { state: "connect-state" });
}
/* own update & state gathering events */
@EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateHostButton(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "host-button" && event.as<"update_state">().state !== "connect-state")
return; return;
const sprops = this.connection?.channelTree.server?.properties; const server_props = this.connection?.channelTree.server?.properties;
if(!sprops || !sprops.virtualserver_hostbutton_gfx_url) { if(!this.connection?.connected || !server_props || !server_props.virtualserver_hostbutton_gfx_url) {
this.event_registry.fire("update_host_button", { this.event_registry.fire("update_host_button", {
url: undefined, url: undefined,
target_url: undefined, target_url: undefined,
@ -453,88 +499,88 @@ export class ControlBar extends React.Component<ControlBarProperties, {}> {
} }
this.event_registry.fire("update_host_button", { this.event_registry.fire("update_host_button", {
url: sprops.virtualserver_hostbutton_gfx_url, url: server_props.virtualserver_hostbutton_gfx_url,
target_url: sprops.virtualserver_hostbutton_url, target_url: server_props.virtualserver_hostbutton_url,
title: sprops.virtualserver_hostbutton_tooltip title: server_props.virtualserver_hostbutton_tooltip
}); });
} }
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"]) @EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateSubscribe(event: Event<ControlBarEvents>) { private updateStateSubscribe(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state") if(event.type === "update_state")
if(event.as<"update_state">().state !== "subscribe-mode") if(event.as<"update_state">().state !== "subscribe-mode")
return; return;
this.event_registry.fire("update_subscribe_state", { this.event_registry.fire("update_subscribe_state", {
subscribeEnabled: !!this.connection?.client_status.channel_subscribe_all subscribeEnabled: !!this.connection?.isSubscribeToAllChannels()
}); });
} }
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"]) @EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateConnect(event: Event<ControlBarEvents>) { private updateStateConnect(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state") if(event.type === "update_state")
if(event.as<"update_state">().state !== "connect-state") if(event.as<"update_state">().state !== "connect-state")
return; return;
this.event_registry.fire("update_connect_state", { this.event_registry.fire("update_connect_state", {
connectedAnywhere: server_connections.server_connection_handlers().findIndex(e => e.connected) !== -1, connectedAnywhere: server_connections.all_connections().findIndex(e => e.connected) !== -1,
connected: !!this.connection?.connected connected: !!this.connection?.connected
}); });
} }
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"]) @EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateAway(event: Event<ControlBarEvents>) { private updateStateAway(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state") if(event.type === "update_state")
if(event.as<"update_state">().state !== "away") if(event.as<"update_state">().state !== "away")
return; return;
const connections = server_connections.server_connection_handlers(); const connections = server_connections.all_connections();
const away_connections = server_connections.server_connection_handlers().filter(e => e.client_status.away); const away_connections = server_connections.all_connections().filter(e => e.isAway());
const away_status = this.connection?.client_status.away; const away_status = !!this.connection?.isAway();
this.event_registry.fire("update_away_state", { this.event_registry.fire("update_away_state", {
awayAnywhere: away_connections.length > 0, awayAnywhere: away_connections.length > 0,
away: typeof away_status === "string" ? true : !!away_status, away: away_status,
awayAll: connections.length === away_connections.length awayAll: connections.length === away_connections.length
}); });
} }
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"]) @EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateMicrophone(event: Event<ControlBarEvents>) { private updateStateMicrophone(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state") if(event.type === "update_state")
if(event.as<"update_state">().state !== "microphone") if(event.as<"update_state">().state !== "microphone")
return; return;
this.event_registry.fire("update_microphone_state", { this.event_registry.fire("update_microphone_state", {
enabled: !!this.connection?.client_status.input_hardware, enabled: !this.connection?.isMicrophoneDisabled(),
muted: this.connection?.client_status.input_muted muted: !!this.connection?.isMicrophoneMuted()
}); });
} }
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"]) @EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateSpeaker(event: Event<ControlBarEvents>) { private updateStateSpeaker(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state") if(event.type === "update_state")
if(event.as<"update_state">().state !== "speaker") if(event.as<"update_state">().state !== "speaker")
return; return;
this.event_registry.fire("update_speaker_state", { this.event_registry.fire("update_speaker_state", {
muted: this.connection?.client_status.output_muted muted: !!this.connection?.isSpeakerMuted()
}); });
} }
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"]) @EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateQuery(event: Event<ControlBarEvents>) { private updateStateQuery(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state") if(event.type === "update_state")
if(event.as<"update_state">().state !== "query") if(event.as<"update_state">().state !== "query")
return; return;
this.event_registry.fire("update_query_state", { this.event_registry.fire("update_query_state", {
queryShown: !!this.connection?.client_status.queries_visible queryShown: !!this.connection?.areQueriesShown()
}); });
} }
@EventHandler<ControlBarEvents>(["update_state_all", "update_state"]) @EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateBookmarks(event: Event<ControlBarEvents>) { private updateStateBookmarks(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state") if(event.type === "update_state")
if(event.as<"update_state">().state !== "bookmarks") if(event.as<"update_state">().state !== "bookmarks")
return; return;
@ -549,7 +595,19 @@ export function control_bar_instance() : ControlBar | undefined {
return react_reference_?.current; return react_reference_?.current;
} }
export type ControlStateUpdateType = "host-button" | "bookmarks" | "subscribe-mode" | "connect-state" | "away" | "microphone" | "speaker" | "query";
export interface ControlBarEvents { export interface ControlBarEvents {
update_state: {
state: "host-button" | "bookmarks" | "subscribe-mode" | "connect-state" | "away" | "microphone" | "speaker" | "query"
},
server_updated: {
handler: ConnectionHandler,
category: "audio" | "settings-initialized" | "connection-state" | "away-status" | "hostbanner"
}
}
export interface InternalControlBarEvents extends ControlBarEvents {
/* update the UI */ /* update the UI */
update_host_button: HostButtonState; update_host_button: HostButtonState;
update_subscribe_state: ChannelSubscribeState; update_subscribe_state: ChannelSubscribeState;
@ -559,20 +617,131 @@ export interface ControlBarEvents {
update_speaker_state: SpeakerState; update_speaker_state: SpeakerState;
update_query_state: QueryState; update_query_state: QueryState;
update_bookmarks: {}, update_bookmarks: {},
update_state: {
state: "host-button" | "bookmarks" | "subscribe-mode" | "connect-state" | "away" | "microphone" | "speaker" | "query"
},
update_state_all: { }, update_state_all: { },
/* trigger actions */
set_connection_handler: { /* UI-Actions */
handler?: ConnectionHandler action_set_subscribe: { subscribe: boolean },
action_disconnect: { globally: boolean },
action_enable_microphone: {}, /* enable/unmute microphone */
action_disable_microphone: {},
action_enable_speaker: {},
action_disable_speaker: {},
action_disable_away: {
globally: boolean
},
action_set_away: {
globally: boolean;
prompt_reason: boolean;
}, },
server_updated: { action_toggle_query: {
handler: ConnectionHandler, shown: boolean
category: "audio" | "settings-initialized" | "connection-state" | "away-status" | "hostbanner" },
action_open_window: {
window: "bookmark-manage" | "query-manage"
},
action_add_current_server_to_bookmarks: {},
/* manly used for the action handler */
set_connection_handler: {
handler?: ConnectionHandler
}
}
function initialize(event_registry: Registry<InternalControlBarEvents>) {
let current_connection_handler: ConnectionHandler;
event_registry.on("set_connection_handler", event => current_connection_handler = event.handler);
event_registry.on("action_disconnect", event => {
(event.globally ? server_connections.all_connections() : [server_connections.active_connection()]).filter(e => !!e).forEach(connection => {
connection.disconnectFromServer();
});
});
event_registry.on("action_set_away", event => {
const set_away = message => {
const value = typeof message === "string" ? message : true;
(event.globally ? server_connections.all_connections() : [server_connections.active_connection()]).filter(e => !!e).forEach(connection => {
connection.setAway(value);
});
settings.changeGlobal(Settings.KEY_CLIENT_STATE_AWAY, true);
settings.changeGlobal(Settings.KEY_CLIENT_AWAY_MESSAGE, typeof value === "boolean" ? "" : value);
};
if(event.prompt_reason) {
createInputModal(tr("Set away message"), tr("Please enter your away message"), () => true, message => {
if(typeof(message) === "string")
set_away(message);
}).open();
} else {
set_away(undefined);
}
});
event_registry.on("action_disable_away", event => {
for(const connection of event.globally ? server_connections.all_connections() : [server_connections.active_connection()]) {
if(!connection) continue;
connection.setAway(false);
} }
//settings-initialized: Update query and channel flags settings.changeGlobal(Settings.KEY_CLIENT_STATE_AWAY, false);
});
event_registry.on(["action_enable_microphone", "action_disable_microphone"], event => {
const state = event.type === "action_enable_microphone";
/* change the default global setting */
settings.changeGlobal(Settings.KEY_CLIENT_STATE_MICROPHONE_MUTED, !state);
if(current_connection_handler) {
current_connection_handler.setMicrophoneMuted(!state);
if(!current_connection_handler.getVoiceRecorder())
current_connection_handler.acquire_recorder(default_recorder, true); /* acquire_recorder already updates the voice status */
}
});
event_registry.on(["action_enable_speaker", "action_disable_speaker"], event => {
const state = event.type === "action_enable_speaker";
/* change the default global setting */
settings.changeGlobal(Settings.KEY_CLIENT_STATE_SPEAKER_MUTED, !state);
current_connection_handler?.setSpeakerMuted(!state);
});
event_registry.on("action_set_subscribe", event => {
/* change the default global setting */
settings.changeGlobal(Settings.KEY_CLIENT_STATE_SUBSCRIBE_ALL_CHANNELS, event.subscribe);
current_connection_handler?.setSubscribeToAllChannels(event.subscribe);
});
event_registry.on("action_toggle_query", event => {
/* change the default global setting */
settings.changeGlobal(Settings.KEY_CLIENT_STATE_QUERY_SHOWN, event.shown);
current_connection_handler?.setQueriesShown(event.shown);
});
event_registry.on("action_add_current_server_to_bookmarks", () => add_server_to_bookmarks(current_connection_handler));
event_registry.on("action_open_window", event => {
switch (event.window) {
case "bookmark-manage":
global_client_actions.fire("action_open_window", { window: "bookmark-manage", connection: current_connection_handler });
return;
case "query-manage":
global_client_actions.fire("action_open_window", { window: "query-manage", connection: current_connection_handler });
return;
}
})
} }

View File

@ -139,7 +139,7 @@ export namespace callbacks {
let client: ClientEntry; let client: ClientEntry;
const current_connection = server_connections.active_connection_handler(); const current_connection = server_connections.active_connection();
if(current_connection && current_connection.channelTree) { if(current_connection && current_connection.channelTree) {
if(!client && client_id) { if(!client && client_id) {
client = current_connection.channelTree.findClient(client_id); client = current_connection.channelTree.findClient(client_id);
@ -175,7 +175,7 @@ export namespace callbacks {
export function callback_context_channel(element: JQuery) { export function callback_context_channel(element: JQuery) {
const channel_id = parseInt(element.attr("channel-id") || "0"); const channel_id = parseInt(element.attr("channel-id") || "0");
const current_connection = server_connections.active_connection_handler(); const current_connection = server_connections.active_connection();
let channel: ChannelEntry; let channel: ChannelEntry;
if(current_connection && current_connection.channelTree) { if(current_connection && current_connection.channelTree) {
channel = current_connection.channelTree.findChannel(channel_id); channel = current_connection.channelTree.findChannel(channel_id);

View File

@ -241,7 +241,7 @@ export function spawnConnectModal(options: {
button_connect.on('click', event => { button_connect.on('click', event => {
modal.close(); modal.close();
const connection = server_connections.active_connection_handler(); const connection = server_connections.active_connection();
if(connection) { if(connection) {
connection.startConnection( connection.startConnection(
current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(), current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(),
@ -259,8 +259,8 @@ export function spawnConnectModal(options: {
button_connect_tab.on('click', event => { button_connect_tab.on('click', event => {
modal.close(); modal.close();
const connection = server_connections.spawn_server_connection_handler(); const connection = server_connections.spawn_server_connection();
server_connections.set_active_connection_handler(connection); server_connections.set_active_connection(connection);
connection.startConnection( connection.startConnection(
current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(), current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(),
selected_profile, selected_profile,

View File

@ -92,7 +92,7 @@ function settings_general_application(container: JQuery, modal: Modal) {
const option = container.find(".option-hostbanner-background") as JQuery<HTMLInputElement>; const option = container.find(".option-hostbanner-background") as JQuery<HTMLInputElement>;
option.on('change', event => { option.on('change', event => {
settings.changeGlobal(Settings.KEY_HOSTBANNER_BACKGROUND, option[0].checked); settings.changeGlobal(Settings.KEY_HOSTBANNER_BACKGROUND, option[0].checked);
for(const sc of server_connections.server_connection_handlers()) for(const sc of server_connections.all_connections())
sc.hostbanner.update(); sc.hostbanner.update();
}).prop("checked", settings.static_global(Settings.KEY_HOSTBANNER_BACKGROUND)); }).prop("checked", settings.static_global(Settings.KEY_HOSTBANNER_BACKGROUND));
} }
@ -384,7 +384,7 @@ function settings_general_chat(container: JQuery, modal: Modal) {
}).prop("checked", settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES)); }).prop("checked", settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES));
} }
const update_format_helper = () => server_connections.server_connection_handlers().map(e => e.side_bar).forEach(e => { const update_format_helper = () => server_connections.all_connections().map(e => e.side_bar).forEach(e => {
e.private_conversations().update_input_format_helper(); e.private_conversations().update_input_format_helper();
e.channel_conversations().update_input_format_helper(); e.channel_conversations().update_input_format_helper();
}); });

View File

@ -2,7 +2,8 @@ import {
AbstractServerConnection, AbstractServerConnection,
CommandOptionDefaults, CommandOptionDefaults,
CommandOptions, CommandOptions,
ConnectionStateListener, voice ConnectionStateListener,
voice
} from "tc-shared/connection/ConnectionBase"; } from "tc-shared/connection/ConnectionBase";
import {ConnectionHandler, ConnectionState, DisconnectReason} from "tc-shared/ConnectionHandler"; import {ConnectionHandler, ConnectionState, DisconnectReason} from "tc-shared/ConnectionHandler";
import {ServerAddress} from "tc-shared/ui/server"; import {ServerAddress} from "tc-shared/ui/server";
@ -10,13 +11,13 @@ import {HandshakeHandler} from "tc-shared/connection/HandshakeHandler";
import {ConnectionCommandHandler, ServerConnectionCommandBoss} from "tc-shared/connection/CommandHandler"; import {ConnectionCommandHandler, ServerConnectionCommandBoss} from "tc-shared/connection/CommandHandler";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {settings, Settings} from "tc-shared/settings"; import {settings, Settings} from "tc-shared/settings";
import {LogCategory} from "tc-shared/log";
import * as log from "tc-shared/log"; import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {Regex} from "tc-shared/ui/modal/ModalConnect"; import {Regex} from "tc-shared/ui/modal/ModalConnect";
import AbstractVoiceConnection = voice.AbstractVoiceConnection;
import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler"; import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
import * as elog from "tc-shared/ui/frames/server_log"; import * as elog from "tc-shared/ui/frames/server_log";
import {VoiceConnection} from "../voice/VoiceHandler"; import {VoiceConnection} from "../voice/VoiceHandler";
import AbstractVoiceConnection = voice.AbstractVoiceConnection;
class ReturnListener<T> { class ReturnListener<T> {
resolve: (value?: T | PromiseLike<T>) => void; resolve: (value?: T | PromiseLike<T>) => void;
@ -304,6 +305,8 @@ export class ServerConnection extends AbstractServerConnection {
} }
async disconnect(reason?: string) : Promise<void> { async disconnect(reason?: string) : Promise<void> {
this.updateConnectionState(ConnectionState.DISCONNECTING);
try {
clearTimeout(this._connect_timeout_timer); clearTimeout(this._connect_timeout_timer);
this._connect_timeout_timer = undefined; this._connect_timeout_timer = undefined;
@ -315,9 +318,6 @@ export class ServerConnection extends AbstractServerConnection {
} }
if(this._connectionState != ConnectionState.UNCONNECTED)
this.updateConnectionState(ConnectionState.UNCONNECTED);
if(this._voice_connection) if(this._voice_connection)
this._voice_connection.drop_rtp_session(); this._voice_connection.drop_rtp_session();
@ -334,6 +334,9 @@ export class ServerConnection extends AbstractServerConnection {
this._connected = false; this._connected = false;
this._retCodeIdx = 0; this._retCodeIdx = 0;
} finally {
this.updateConnectionState(ConnectionState.UNCONNECTED);
}
} }
private handle_socket_message(data) { private handle_socket_message(data) {