diff --git a/ChangeLog.md b/ChangeLog.md index d2c7f414..872ebb4c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,10 @@ # 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** - Implemented the new music bot playlist song list - Implemented the missing server log message builders diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 96f4b71b..be3c1946 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -2,15 +2,14 @@ import {ChannelTree} from "tc-shared/ui/view"; import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase"; import {PermissionManager} from "tc-shared/permission/PermissionManager"; 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 {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 {ServerAddress} from "tc-shared/ui/server"; 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 {createErrorModal, createInfoModal, createInputModal, Modal} from "tc-shared/ui/elements/Modal"; import {hashPassword} from "tc-shared/utils/helpers"; 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 dns from "tc-backend/dns"; 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 { HANDLER_DESTROYED, @@ -54,11 +54,25 @@ export enum DisconnectReason { } export enum ConnectionState { - UNCONNECTED, - CONNECTING, - INITIALISING, - CONNECTED, - DISCONNECTING + UNCONNECTED, /* no connection is currenting running */ + CONNECTING, /* we try to establish a connection to the target server */ + INITIALISING, /* we're setting up the connection encryption */ + AUTHENTICATING, /* we're authenticating ourself so we get a unique ID */ + 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 { @@ -76,7 +90,7 @@ export enum ViewReasonId { VREASON_SERVER_SHUTDOWN = 11 } -export interface VoiceStatus { +export interface LocalClientStatus { input_hardware: boolean; input_muted: boolean; output_muted: boolean; @@ -106,6 +120,7 @@ export interface ConnectParameters { declare const native_client; export class ConnectionHandler { + private readonly event_registry: Registry; channelTree: ChannelTree; serverConnection: AbstractServerConnection; @@ -132,7 +147,7 @@ export class ConnectionHandler { private _connect_initialize_id: number = 1; - client_status: VoiceStatus = { + private client_status: LocalClientStatus = { input_hardware: false, input_muted: false, output_muted: false, @@ -150,6 +165,9 @@ export class ConnectionHandler { log: ServerLog; constructor() { + this.event_registry = new Registry(); + this.event_registry.enable_debug("connection-handler"); + this.settings = new ServerSettings(); this.log = new ServerLog(this); @@ -178,14 +196,30 @@ export class ConnectionHandler { if(event.isDefaultPrevented()) return; - server_connections.set_active_connection_handler(this); + server_connections.set_active_connection(this); }); 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(); }); 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 { + return this.event_registry; } tab_set_name(name: string) { @@ -193,8 +227,6 @@ export class ConnectionHandler { this.tag_connection_handler.find(".server-name").text(name); } - setup() { } - async startConnection(addr: string, profile: ConnectionProfile, user_action: boolean, parameters: ConnectParameters) { this.tab_set_name(tr("Connecting")); this.cancel_reconnect(false); @@ -283,6 +315,17 @@ export class ConnectionHandler { }, 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; } getClientId() { return this._clientId; } @@ -299,16 +342,20 @@ export class ConnectionHandler { getServerConnection() : AbstractServerConnection { return this.serverConnection; } - /** - * LISTENER - */ - onConnected() { + @EventHandler("notify_connection_state_changed") + private handleConnectionConnected(event: ConnectionEvents["notify_connection_state_changed"]) { + if(event.new_state !== ConnectionState.CONNECTED) return; 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(); if(this.groups.serverGroups.length == 0) this.groups.requestGroups(); - this.initialize_server_settings(); + this.settings.setServer(this.channelTree.server.properties.virtualserver_unique_identifier); /* apply the server settings */ 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 { return this.serverConnection && this.serverConnection.connected(); } @@ -606,7 +632,6 @@ export class ConnectionHandler { if(this.serverConnection) this.serverConnection.disconnect(); - this.on_connection_state_changed(); /* really required to call? */ this.side_bar.private_conversations().clear_client_ids(); this.hostbanner.update(); @@ -639,12 +664,16 @@ export class ConnectionHandler { } } - private on_connection_state_changed() { - control_bar_instance()?.events().fire("server_updated", { category: "connection-state", handler: this }); + private on_connection_state_changed(old_state: ConnectionState, new_state: ConnectionState) { + this.event_registry.fire("notify_connection_state_changed", { + old_state: old_state, + new_state: new_state + }); } private _last_record_error_popup: number; update_voice_status(targetChannel?: ChannelEntry) { + //TODO: Simplify this if(!this._local_client) return; /* we've been destroyed */ targetChannel = targetChannel || this.getClient().currentChannel(); @@ -748,9 +777,14 @@ export class ConnectionHandler { } } - - control_bar_instance()?.events().fire("server_updated", { category: "audio", handler: this }); - top_menu.update_state(); //TODO: Only run "small" update? + //TODO: Only trigger events for stuff which has been updated + this.event_registry.fire("notify_state_updated", { + 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() { @@ -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() { this.channelTree.handle_resized(); this.invoke_resized_on_activate = false; } 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(); (vconnection ? vconnection.acquire_voice_recorder(voice_recoder) : Promise.resolve()).catch(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 { const name = (this.getClient() ? this.getClient().clientNickName() : "") || (this.serverConnection && this.serverConnection.handshake_handler() ? this.serverConnection.handshake_handler().parameters.nickname : "") || @@ -913,6 +927,7 @@ export class ConnectionHandler { } destroy() { + this.event_registry.unregister_handler(this); this.cancel_reconnect(true); this.tag_connection_handler && this.tag_connection_handler.remove(); @@ -954,4 +969,107 @@ export class ConnectionHandler { this.sound = 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 + } } \ No newline at end of file diff --git a/shared/js/bookmarks.ts b/shared/js/bookmarks.ts index a114778c..9dee583a 100644 --- a/shared/js/bookmarks.ts +++ b/shared/js/bookmarks.ts @@ -5,14 +5,15 @@ import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/ import {default_profile, find_profile} from "tc-shared/profiles/ConnectionProfile"; import {server_connections} from "tc-shared/ui/frames/connection_handlers"; 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 {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) => { const profile = find_profile(mark.connect_profile) || default_profile(); if(profile.valid()) { - const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection_handler() : server_connections.spawn_server_connection_handler(); - server_connections.set_active_connection_handler(connection); + const connection = (typeof(new_tab) !== "boolean" || !new_tab) ? server_connections.active_connection() : server_connections.spawn_server_connection(); + server_connections.set_active_connection(connection); connection.startConnection( mark.server_properties.server_address + ":" + mark.server_properties.server_port, profile, @@ -232,23 +233,23 @@ export function delete_bookmark(bookmark: Bookmark | DirectoryBookmark) { delete_bookmark_recursive(bookmarks(), bookmark) } -export function add_current_server() { - const ch = server_connections.active_connection_handler(); - if(ch && ch.connected) { - const ce = ch.getClient(); +export function add_server_to_bookmarks(server: ConnectionHandler) { + if(server && server.connected) { + const ce = server.getClient(); const name = ce ? ce.clientNickName() : undefined; createInputModal(tr("Enter bookmarks name"), tr("Please enter the bookmarks name:
"), text => text.length > 0, result => { if(result) { const bookmark = create_bookmark(result as string, bookmarks(), { - server_port: ch.serverConnection.remote_address().port, - server_address: ch.serverConnection.remote_address().host, + server_port: server.serverConnection.remote_address().port, + server_address: server.serverConnection.remote_address().host, server_password: "", server_password_hash: "" }, name); save_bookmark(bookmark); - control_bar.update_bookmarks(); + control_bar_instance().events().fire("update_state", { state: "bookmarks" }); + //control_bar.update_bookmarks(); top_menu.rebuild_bookmarks(); createInfoModal(tr("Server added"), tr("Server has been successfully added to your bookmarks.")).open(); diff --git a/shared/js/connection/CommandHandler.ts b/shared/js/connection/CommandHandler.ts index ff8767c3..e10c18bc 100644 --- a/shared/js/connection/CommandHandler.ts +++ b/shared/js/connection/CommandHandler.ts @@ -1,11 +1,9 @@ import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; import * as server_log from "tc-shared/ui/frames/server_log"; -import { - AbstractServerConnection, CommandOptions, ServerCommand -} from "tc-shared/connection/ConnectionBase"; +import {AbstractServerConnection, CommandOptions, ServerCommand} from "tc-shared/connection/ConnectionBase"; import {Sound} from "tc-shared/sound/Sounds"; 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 { ClientConnectionInfo, @@ -16,7 +14,7 @@ import { SongInfo } from "tc-shared/ui/client"; 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 {server_connections} from "tc-shared/ui/frames/connection_handlers"; import {spawnPoke} from "tc-shared/ui/modal/ModalPoke"; @@ -247,7 +245,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { 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.
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; - const scon = server_connections.active_connection_handler(); + const scon = server_connections.active_connection(); if(scon.serverConnection.connected) scon.serverConnection.send_command("tokenuse", { @@ -260,11 +258,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { }, { field_placeholder: tr("Enter Privilege Key") }).open(); } - this.connection_handler.log.log(server_log.Type.CONNECTION_CONNECTED, { - own_client: this.connection_handler.getClient().log_data() - }); - this.connection_handler.sound.play(Sound.CONNECTION_CONNECTED); - this.connection.client.onConnected(); + this.connection.updateConnectionState(ConnectionState.CONNECTED); } handleNotifyServerConnectionInfo(json) { diff --git a/shared/js/connection/ConnectionBase.ts b/shared/js/connection/ConnectionBase.ts index ef401af2..512807dd 100644 --- a/shared/js/connection/ConnectionBase.ts +++ b/shared/js/connection/ConnectionBase.ts @@ -47,6 +47,9 @@ export abstract class AbstractServerConnection { abstract remote_address() : ServerAddress; /* 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() : { native: number, javascript?: number diff --git a/shared/js/events.ts b/shared/js/events.ts index f8db3238..a7abe1b3 100644 --- a/shared/js/events.ts +++ b/shared/js/events.ts @@ -110,6 +110,7 @@ export class Registry { fire(event_type: T, data?: Events[T]) { 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, { type: event_type, as: function () { return this; } @@ -130,7 +131,7 @@ export class Registry { invoke_count++; } 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 { let registered_events = {}; for(const function_name of Object.getOwnPropertyNames(proto)) { if(function_name === "constructor") continue; + if(typeof proto[function_name] !== "function") continue; if(typeof proto[function_name][event_annotation_key] !== "object") continue; const event_data = proto[function_name][event_annotation_key]; diff --git a/shared/js/events/ClientGlobalControlHandler.ts b/shared/js/events/ClientGlobalControlHandler.ts index 50a4a3e3..bd509bec 100644 --- a/shared/js/events/ClientGlobalControlHandler.ts +++ b/shared/js/events/ClientGlobalControlHandler.ts @@ -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 {default_recorder} from "tc-shared/voice/RecorderProfile"; 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 PermissionType from "tc-shared/permission/PermissionType"; 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 {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings"; +/* function initialize_sounds(event_registry: Registry) { { let microphone_muted = undefined; @@ -38,102 +39,16 @@ function initialize_sounds(event_registry: Registry) } } -function load_default_states() { - this.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) }); +export function load_default_states(event_registry: Registry) { + event_registry.fire("action_toggle_speaker", { state: settings.static_global(Settings.KEY_CONTROL_MUTE_OUTPUT, false) }); + event_registry.fire("action_toggle_microphone", { state: settings.static_global(Settings.KEY_CONTROL_MUTE_INPUT, false) }); } +*/ export function initialize(event_registry: Registry) { let current_connection_handler: ConnectionHandler | undefined; - event_registry.on("action_set_active_connection_handler", event => { current_connection_handler = event.handler; }); - - 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 - }); - }); + server_connections.events().on("notify_active_handler_changed", event => current_connection_handler = event.new_handler); + //initialize_sounds(event_registry); event_registry.on("action_open_window", event => { const handle_import_error = error => { @@ -233,5 +148,9 @@ export function initialize(event_registry: Registry) } }); - load_default_states(); + event_registry.on("action_open_window_connect", event => { + spawnConnectModal({ + default_connect_new_tab: event.new_tab + }); + }); } \ No newline at end of file diff --git a/shared/js/events/GlobalEvents.ts b/shared/js/events/GlobalEvents.ts index 3236cb55..5c2a2215 100644 --- a/shared/js/events/GlobalEvents.ts +++ b/shared/js/events/GlobalEvents.ts @@ -1,49 +1,17 @@ import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {Registry} from "tc-shared/events"; export interface ClientGlobalControlEvents { - action_set_channel_subscribe_mode: { - 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 - }, - + /* open a basic window */ action_open_window: { window: "bookmark-manage" | "query-manage" | "query-create" | "ban-list" | "permissions" | "token-list" | "token-use" | "settings", connection?: ConnectionHandler }, - action_add_current_server_to_bookmarks: {}, - action_set_active_connection_handler: { - handler?: ConnectionHandler - }, - - - //TODO - notify_microphone_state_changed: { - state: boolean + /* some more specific window openings */ + action_open_window_connect: { + new_tab: boolean } -} \ No newline at end of file +} + +export const global_client_actions = new Registry(); \ No newline at end of file diff --git a/shared/js/main.tsx b/shared/js/main.tsx index 70a24c0a..c49bc584 100644 --- a/shared/js/main.tsx +++ b/shared/js/main.tsx @@ -15,7 +15,7 @@ import * as stats from "./stats"; import * as fidentity from "./profiles/identities/TeaForumIdentity"; import {default_recorder, RecorderProfile, set_default_recorder} from "tc-shared/voice/RecorderProfile"; 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 * as top_menu from "./ui/frames/MenuBar"; 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 ReactDOM from "react-dom"; import * as cbar from "./ui/frames/control-bar"; +import * as global_ev_handler from "./events/ClientGlobalControlHandler"; 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 */ require("./proto").initialize(); @@ -52,14 +53,14 @@ function setup_close() { profiles.save(); 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(!native_client) { event.returnValue = "Are you really sure?
You're still connected!"; } else { const do_exit = () => { - const dp = server_connections.server_connection_handlers().map(e => { + const dp = server_connections.all_connections().map(e => { if(e.serverConnection.connected()) return e.serverConnection.disconnect(tr("client closed")); return Promise.resolve(); @@ -146,8 +147,6 @@ async function initialize() { bipc.setup(); } -export let client_control_events: Registry; - async function initialize_app() { try { //Initialize main template const main = $("#tmpl_main").renderTag({ @@ -161,16 +160,22 @@ async function initialize_app() { loader.critical_error(tr("Failed to setup main page!")); return; } - - client_control_events = new Registry(); + cmanager.initialize(); + global_ev_handler.initialize(global_client_actions); { const bar = ( ); 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()) console.warn(tr("Failed to initialize audio controller!")); @@ -266,7 +271,7 @@ export function handle_connect_request(properties: bipc.connect.ConnectRequestDa hashed: password_hashed } : undefined }); - server_connections.set_active_connection_handler(connection); + server_connections.set_active_connection(connection); } else { spawnConnectModal({},{ url: properties.address, @@ -311,12 +316,9 @@ function main() { top_menu.initialize(); - cmanager.initialize(new ServerConnectionManager($("#connection-handlers"))); - control_bar.control_bar.initialise(); /* before connection handler to allow property apply */ - - const initial_handler = server_connections.spawn_server_connection_handler(); + const initial_handler = server_connections.spawn_server_connection(); 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 **/ fidentity.update_forum(); @@ -328,9 +330,9 @@ function main() { if(_resize_timeout) clearTimeout(_resize_timeout); _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; - const active_connection = server_connections.active_connection_handler(); + const active_connection = server_connections.active_connection(); if(active_connection) active_connection.resize_elements(); $(".window-resize-listener").trigger('resize'); @@ -346,13 +348,13 @@ function main() { 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) => { message = message || "Hello World"; - const connection = server_connections.active_connection_handler(); + const connection = server_connections.active_connection(); connection.fileManager.upload_file({ size: message.length, 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 */ setTimeout(() => { - const connection = server_connections.active_connection_handler(); + const connection = server_connections.active_connection(); /* 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, { 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") }); } @@ -523,7 +525,7 @@ const task_connect_handler: loader.Task = { }; 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; } } diff --git a/shared/js/settings.ts b/shared/js/settings.ts index 86e56622..aa00073d 100644 --- a/shared/js/settings.ts +++ b/shared/js/settings.ts @@ -184,18 +184,34 @@ export class Settings extends StaticSettings { description: 'Triggers a loading error at the end of the loading process.' }; - /* Control bar */ - static readonly KEY_CONTROL_MUTE_INPUT: SettingsKey = { - key: 'mute_input' + /* Default client states */ + static readonly KEY_CLIENT_STATE_MICROPHONE_MUTED: SettingsKey = { + key: 'client_state_microphone_muted', + default_value: false, + fallback_keys: ["mute_input"] }; - static readonly KEY_CONTROL_MUTE_OUTPUT: SettingsKey = { - key: 'mute_output' + static readonly KEY_CLIENT_STATE_SPEAKER_MUTED: SettingsKey = { + key: 'client_state_speaker_muted', + default_value: false, + fallback_keys: ["mute_output"] }; - static readonly KEY_CONTROL_SHOW_QUERIES: SettingsKey = { - key: 'show_server_queries' + static readonly KEY_CLIENT_STATE_QUERY_SHOWN: SettingsKey = { + key: 'client_state_query_shown', + default_value: false, + fallback_keys: ["show_server_queries"] }; - static readonly KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL: SettingsKey = { - key: 'channel_subscribe_all' + static readonly KEY_CLIENT_STATE_SUBSCRIBE_ALL_CHANNELS: SettingsKey = { + key: 'client_state_subscribe_all_channels', + default_value: true, + fallback_keys: ["channel_subscribe_all"] + }; + static readonly KEY_CLIENT_STATE_AWAY: SettingsKey = { + key: 'client_state_away', + default_value: false + }; + static readonly KEY_CLIENT_AWAY_MESSAGE: SettingsKey = { + key: 'client_away_message', + default_value: "" }; /* Connect parameters */ @@ -367,6 +383,8 @@ export class Settings extends StaticSettings { static initialize() { settings = new Settings(); + (window as any).settings = settings; + (window as any).Settings = Settings; } private cacheGlobal = {}; @@ -417,9 +435,7 @@ export class Settings extends StaticSettings { changeGlobal(key: string | SettingsKey, value?: T){ key = Settings.keyify(key); - - - if(this.cacheGlobal[key.key] == value) return; + if(this.cacheGlobal[key.key] === value) return; this.updated = true; this.cacheGlobal[key.key] = StaticSettings.transformOtS(value); diff --git a/shared/js/ui/elements/ReactComponentBase.ts b/shared/js/ui/elements/ReactComponentBase.ts index 1508367d..9f349c6d 100644 --- a/shared/js/ui/elements/ReactComponentBase.ts +++ b/shared/js/ui/elements/ReactComponentBase.ts @@ -12,6 +12,8 @@ export abstract class ReactComponentBase extends React.Compon protected abstract default_state() : State; 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)); } diff --git a/shared/js/ui/frames/MenuBar.ts b/shared/js/ui/frames/MenuBar.ts index 19d5af51..bc6b15a2 100644 --- a/shared/js/ui/frames/MenuBar.ts +++ b/shared/js/ui/frames/MenuBar.ts @@ -1,7 +1,7 @@ import {Icon, IconManager} from "tc-shared/FileManager"; import {spawnBookmarkModal} from "tc-shared/ui/modal/ModalBookmarks"; import { - add_current_server, + add_server_to_bookmarks, Bookmark, bookmarks, BookmarkType, @@ -23,7 +23,6 @@ import {spawnAbout} from "tc-shared/ui/modal/ModalAbout"; import {server_connections} from "tc-shared/ui/frames/connection_handlers"; import * as loader from "tc-loader"; 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"; 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.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]}; } @@ -320,7 +319,7 @@ export function update_state() { } 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; }; @@ -341,13 +340,8 @@ export function initialize() { item.click(() => spawnConnectModal({})); const do_disconnect = (handlers: ConnectionHandler[]) => { - for(const handler of handlers) { - handler.cancel_reconnect(true); - 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, {}); - } + for(const handler of handlers) + handler.disconnectFromServer(); control_bar_instance()?.events().fire("update_state", { state: "connect-state" }); update_state(); @@ -356,7 +350,7 @@ export function initialize() { item.icon('client-disconnect'); item.disabled(true); item.click(() => { - const handler = server_connections.active_connection_handler(); + const handler = server_connections.active_connection(); do_disconnect([handler]); }); _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.icon('client-disconnect'); 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) => { - item.visible(server_connections && server_connections.server_connection_handlers().length > 1); + item.visible(server_connections && server_connections.all_connections().length > 1); return true; }}; @@ -394,35 +388,35 @@ export function initialize() { item = menu.append_item(tr("Server Groups")); item.icon("client-permission_server_groups"); 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]}; item = menu.append_item(tr("Client Permissions")); item.icon("client-permission_client"); 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]}; item = menu.append_item(tr("Channel Client Permissions")); item.icon("client-permission_client"); 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]}; item = menu.append_item(tr("Channel Groups")); item.icon("client-permission_channel"); 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]}; item = menu.append_item(tr("Channel Permissions")); item.icon("client-permission_channel"); 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]}; @@ -440,7 +434,7 @@ export function initialize() { //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 => { if(!result) return; - const scon = server_connections.active_connection_handler(); + const scon = server_connections.active_connection(); if(scon.serverConnection.connected) scon.serverConnection.send_command("tokenuse", { @@ -476,7 +470,7 @@ export function initialize() { item = menu.append_item(tr("Ban List")); item.icon('client-ban_list'); item.click(() => { - const scon = server_connections.active_connection_handler(); + const scon = server_connections.active_connection(); if(scon && scon.connected) { if(scon.permissions.neededPermission(PermissionType.B_CLIENT_BAN_LIST).granted(1)) { openBanList(scon); @@ -493,7 +487,7 @@ export function initialize() { item = menu.append_item(tr("Query List")); item.icon('client-server_query'); item.click(() => { - const scon = server_connections.active_connection_handler(); + const scon = server_connections.active_connection(); 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)) { spawnQueryManage(scon); @@ -510,7 +504,7 @@ export function initialize() { item = menu.append_item(tr("Query Create")); item.icon('client-server_query'); item.click(() => { - const scon = server_connections.active_connection_handler(); + const scon = server_connections.active_connection(); 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)) { spawnQueryCreate(scon); diff --git a/shared/js/ui/frames/connection_handlers.ts b/shared/js/ui/frames/connection_handlers.ts index cbc8139b..93a77827 100644 --- a/shared/js/ui/frames/connection_handlers.ts +++ b/shared/js/ui/frames/connection_handlers.ts @@ -1,14 +1,15 @@ import {ConnectionHandler, DisconnectReason} from "tc-shared/ConnectionHandler"; import {Settings, settings} from "tc-shared/settings"; import * as top_menu from "./MenuBar"; -import {control_bar_instance} from "tc-shared/ui/frames/control-bar"; -import {client_control_events} from "tc-shared/main"; +import {Registry} from "tc-shared/events"; -export let server_connections: ServerConnectionManager; -export function initialize(manager: ServerConnectionManager) { - server_connections = manager; +export let server_connections: ConnectionManager; +export function initialize() { + 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; private connection_handlers: ConnectionHandler[] = []; private active_handler: ConnectionHandler | undefined; @@ -23,7 +24,20 @@ export class ServerConnectionManager { private _tag_button_scoll_right: 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) { + this.event_registry = new Registry(); + this.event_registry.enable_debug("connection-manager"); + this._tag = tag; if(settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION, false)) @@ -42,23 +56,42 @@ export class ServerConnectionManager { this._container_hostbanner = $("#hostbanner"); this._container_chat = $("#chat"); - this.set_active_connection_handler(undefined); + this.set_active_connection(undefined); } - spawn_server_connection_handler() : ConnectionHandler { + events() : Registry { + return this.event_registry; + } + + spawn_server_connection() : ConnectionHandler { const handler = new ConnectionHandler(); + handler.initialize_client_state(this.active_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); this._tag.toggleClass("shown", this.connection_handlers.length > 1); this._update_scroll(); + + this.event_registry.fire("notify_handler_created", { handler: handler }); return handler; } - destroy_server_connection_handler(handler: ConnectionHandler) { - this.connection_handlers.remove(handler); + destroy_server_connection(handler: ConnectionHandler) { + 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(); this._update_scroll(); this._tag.toggleClass("shown", this.connection_handlers.length > 1); @@ -70,16 +103,22 @@ export class ServerConnectionManager { } 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 */ handler.destroy(); } - set_active_connection_handler(handler: ConnectionHandler) { + set_active_connection(handler: ConnectionHandler) { if(handler && this.connection_handlers.indexOf(handler) == -1) 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._container_channel_tree.children().detach(); this._container_chat.children().detach(); @@ -97,16 +136,20 @@ export class ServerConnectionManager { if(handler.invoke_resized_on_activate) handler.resize_elements(); } + const old_handler = this.active_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! - top_menu.update_state(); + this.event_registry.fire("notify_active_handler_changed", { + 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; } - server_connection_handlers() : ConnectionHandler[] { + all_connections() : ConnectionHandler[] { return this.connection_handlers; } @@ -140,4 +183,22 @@ export class ServerConnectionManager { this._tag_button_scoll_left.toggleClass("disabled", scroll <= 0); 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 + } } \ No newline at end of file diff --git a/shared/js/ui/frames/control-bar/actions.ts b/shared/js/ui/frames/control-bar/actions.ts deleted file mode 100644 index c64f7c30..00000000 --- a/shared/js/ui/frames/control-bar/actions.ts +++ /dev/null @@ -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) { - { - 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) => { - initialize_sounds(event_registry); -}; - -//TODO: Left action handler! \ No newline at end of file diff --git a/shared/js/ui/frames/control-bar/button.scss b/shared/js/ui/frames/control-bar/button.scss index ad440d90..0c1260c9 100644 --- a/shared/js/ui/frames/control-bar/button.scss +++ b/shared/js/ui/frames/control-bar/button.scss @@ -1,39 +1,54 @@ @import "../../../../css/static/properties"; @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 */ .button, .dropdownArrow { text-align: center; - border: .05em solid rgba(0, 0, 0, 0); + border: .05em solid var(--menu-bar-button-border); border-radius: $border_radius_small; - background-color: #454545; + background-color: var(--menu-bar-button-background); &:hover { - background-color: #393c43; - border-color: #4a4c55; - /*box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19);*/ + background-color: var(--menu-bar-button-background-hover); + 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); */ } &.activated { - background-color: #2f3841; - border-color: #005fa1; + background-color: var(--menu-bar-button-background-activated); + border-color: var(--menu-bar-button-border-activated); &:hover { - background-color: #263340; - border-color: #005fa1; + background-color: var(--menu-bar-button-background-activated-hover); + border-color: var(--menu-bar-button-border-activated-hover); } &.theme-red { - background-color: #412f2f; - border-color: #a10000; + background-color: var(--menu-bar-button-background-activated-red); + border-color: var(--menu-bar-button-border-activated-red); &:hover { - background-color: #402626; - border-color: #a10000; + background-color: var(--menu-bar-button-background-activated-red-hover); + border-color: var(--menu-bar-button-border-activated-red-hover); } } } diff --git a/shared/js/ui/frames/control-bar/button.tsx b/shared/js/ui/frames/control-bar/button.tsx index 81508f7b..40a2aed5 100644 --- a/shared/js/ui/frames/control-bar/button.tsx +++ b/shared/js/ui/frames/control-bar/button.tsx @@ -78,7 +78,7 @@ export class Button extends ReactComponentBase { } 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); if(this.props.autoSwitch) this.updateState({ switched: typeof result === "boolean" ? result : new_state }); diff --git a/shared/js/ui/frames/control-bar/index.scss b/shared/js/ui/frames/control-bar/index.scss index 3a827242..78da44ec 100644 --- a/shared/js/ui/frames/control-bar/index.scss +++ b/shared/js/ui/frames/control-bar/index.scss @@ -1,6 +1,11 @@ @import "../../../../css/static/properties"; @import "../../../../css/static/mixin"; +/* Variables */ +html:root { + --menu-bar-background: #454545; +} + /* max height is 2em */ .controlBar { display: flex; @@ -10,6 +15,7 @@ height: 100%; align-items: center; + background: var(--menu-bar-background); /* tmp fix for ultra small devices */ overflow-y: visible; diff --git a/shared/js/ui/frames/control-bar/index.tsx b/shared/js/ui/frames/control-bar/index.tsx index e2cef2a9..92c79275 100644 --- a/shared/js/ui/frames/control-bar/index.tsx +++ b/shared/js/ui/frames/control-bar/index.tsx @@ -3,11 +3,12 @@ import {Button} from "./button"; import {DropdownEntry} from "tc-shared/ui/frames/control-bar/dropdown"; import {Translatable} from "tc-shared/ui/elements/i18n"; 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 {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 { + add_server_to_bookmarks, Bookmark, bookmarks, BookmarkType, @@ -17,8 +18,9 @@ import { } from "tc-shared/bookmarks"; import {IconManager} from "tc-shared/FileManager"; import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; -import {client_control_events} from "tc-shared/main"; -const register_actions = require("./actions"); +import {createInputModal} from "tc-shared/ui/elements/Modal"; +import {default_recorder} from "tc-shared/voice/RecorderProfile"; +import {global_client_actions} from "tc-shared/events/GlobalEvents"; const cssStyle = require("./index.scss"); const cssButtonStyle = require("./button.scss"); @@ -29,7 +31,7 @@ export interface ConnectionState { } @ReactEventHandler(obj => obj.props.event_registry) -class ConnectButton extends ReactComponentBase<{ multiSession: boolean; event_registry: Registry }, ConnectionState> { +class ConnectButton extends ReactComponentBase<{ multiSession: boolean; event_registry: Registry }, ConnectionState> { protected default_state(): ConnectionState { return { connected: false, @@ -43,51 +45,51 @@ class ConnectButton extends ReactComponentBase<{ multiSession: boolean; event_re if(!this.state.connected) { subentries.push( } - onClick={ () => client_control_events.fire("action_open_connect", { new_tab: false }) } /> + onClick={ () => global_client_actions.fire("action_open_window_connect", {new_tab: false }) } /> ); } else { subentries.push( - } - onClick={ () => client_control_events.fire("action_disconnect", { globally: false }) }/> + } + onClick={ () => this.props.event_registry.fire("action_disconnect", { globally: false }) }/> ); } if(this.state.connectedAnywhere) { subentries.push( - } - onClick={ () => client_control_events.fire("action_disconnect", { globally: true }) }/> + } + onClick={ () => this.props.event_registry.fire("action_disconnect", { globally: true }) }/> ); } subentries.push( } - 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) { return ( ); } else { return ( ); } } - @EventHandler("update_connect_state") + @EventHandler("update_connect_state") private handleStateUpdate(state: ConnectionState) { this.updateState(state); } } @ReactEventHandler(obj => obj.props.event_registry) -class BookmarkButton extends ReactComponentBase<{ event_registry: Registry }, {}> { +class BookmarkButton extends ReactComponentBase<{ event_registry: Registry }, {}> { private button_ref: React.RefObject ) @@ -160,7 +163,7 @@ class BookmarkButton extends ReactComponentBase<{ event_registry: Registry("update_bookmarks") + @EventHandler("update_bookmarks") private handleStateUpdate() { this.forceUpdate(); } @@ -173,7 +176,7 @@ export interface AwayState { } @ReactEventHandler(obj => obj.props.event_registry) -class AwayButton extends ReactComponentBase<{ event_registry: Registry }, AwayState> { +class AwayButton extends ReactComponentBase<{ event_registry: Registry }, AwayState> { protected default_state(): AwayState { return { away: false, @@ -186,35 +189,42 @@ class AwayButton extends ReactComponentBase<{ event_registry: Registry} - onClick={() => client_control_events.fire("action_disable_away", { globally: false })} />); + onClick={() => this.props.event_registry.fire("action_disable_away", { globally: false })} />); } else { dropdowns.push(} - 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(} - 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(
); if(this.state.awayAnywhere) { dropdowns.push(} - 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) { dropdowns.push(} - 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(} - 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 */ return ( - ); } - @EventHandler("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("update_away_state") private handleStateUpdate(state: AwayState) { this.updateState(state); } @@ -225,17 +235,17 @@ export interface ChannelSubscribeState { } @ReactEventHandler(obj => obj.props.event_registry) -class ChannelSubscribeButton extends ReactComponentBase<{ event_registry: Registry }, ChannelSubscribeState> { +class ChannelSubscribeButton extends ReactComponentBase<{ event_registry: Registry }, ChannelSubscribeState> { protected default_state(): ChannelSubscribeState { return { subscribeEnabled: false }; } render() { return ) } - @EventHandler("update_query_state") + @EventHandler("update_query_state") private handleStateUpdate(state: QueryState) { this.updateState(state); } @@ -341,7 +351,7 @@ export interface HostButtonState { } @ReactEventHandler(obj => obj.props.event_registry) -class HostButton extends ReactComponentBase<{ event_registry: Registry }, HostButtonState> { +class HostButton extends ReactComponentBase<{ event_registry: Registry }, HostButtonState> { protected default_state() { return { url: undefined, @@ -370,7 +380,7 @@ class HostButton extends ReactComponentBase<{ event_registry: Registry("update_host_button") + @EventHandler("update_host_button") private handleStateUpdate(state: HostButtonState) { this.updateState(state); } @@ -382,33 +392,25 @@ export interface ControlBarProperties { @ReactEventHandler(obj => obj.event_registry) export class ControlBar extends React.Component { - private readonly event_registry: Registry; + private readonly event_registry: Registry; 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) { super(props); - this.event_registry = new Registry(); + this.event_registry = new Registry(); this.event_registry.enable_debug("control-bar"); - register_actions(this.event_registry); - + initialize(this.event_registry); } - componentDidMount(): void { - - } - - /* - 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 { return this.event_registry; } + events() : Registry { return this.event_registry; } render() { return ( @@ -428,22 +430,66 @@ export class ControlBar extends React.Component { ) } - @EventHandler("set_connection_handler") - private handleSetConnectionHandler(event: ControlBarEvents["set_connection_handler"]) { - if(this.connection == event.handler) return; + private handleActiveConnectionHandlerChanged(event: ConnectionManagerEvents["notify_active_handler_changed"]) { + if(event.old_handler) + 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"); } - @EventHandler(["update_state_all", "update_state"]) - private updateStateHostButton(event: Event) { - if(event.type === "update_state")4 - if(event.as<"update_state">().state !== "host-button") + private unregisterConnectionHandlerEvents(target: ConnectionHandler) { + const events = target.events(); + events.off("notify_state_updated", this.connection_handler_callbacks.notify_state_updated); + 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(["update_state_all", "update_state"]) + private updateStateHostButton(event: Event) { + if(event.type === "update_state") + if(event.as<"update_state">().state !== "host-button" && event.as<"update_state">().state !== "connect-state") return; - const sprops = this.connection?.channelTree.server?.properties; - if(!sprops || !sprops.virtualserver_hostbutton_gfx_url) { + const server_props = this.connection?.channelTree.server?.properties; + if(!this.connection?.connected || !server_props || !server_props.virtualserver_hostbutton_gfx_url) { this.event_registry.fire("update_host_button", { url: undefined, target_url: undefined, @@ -453,88 +499,88 @@ export class ControlBar extends React.Component { } this.event_registry.fire("update_host_button", { - url: sprops.virtualserver_hostbutton_gfx_url, - target_url: sprops.virtualserver_hostbutton_url, - title: sprops.virtualserver_hostbutton_tooltip + url: server_props.virtualserver_hostbutton_gfx_url, + target_url: server_props.virtualserver_hostbutton_url, + title: server_props.virtualserver_hostbutton_tooltip }); } - @EventHandler(["update_state_all", "update_state"]) - private updateStateSubscribe(event: Event) { + @EventHandler(["update_state_all", "update_state"]) + private updateStateSubscribe(event: Event) { if(event.type === "update_state") if(event.as<"update_state">().state !== "subscribe-mode") return; this.event_registry.fire("update_subscribe_state", { - subscribeEnabled: !!this.connection?.client_status.channel_subscribe_all + subscribeEnabled: !!this.connection?.isSubscribeToAllChannels() }); } - @EventHandler(["update_state_all", "update_state"]) - private updateStateConnect(event: Event) { + @EventHandler(["update_state_all", "update_state"]) + private updateStateConnect(event: Event) { if(event.type === "update_state") if(event.as<"update_state">().state !== "connect-state") return; 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 }); } - @EventHandler(["update_state_all", "update_state"]) - private updateStateAway(event: Event) { + @EventHandler(["update_state_all", "update_state"]) + private updateStateAway(event: Event) { if(event.type === "update_state") if(event.as<"update_state">().state !== "away") return; - const connections = server_connections.server_connection_handlers(); - const away_connections = server_connections.server_connection_handlers().filter(e => e.client_status.away); + const connections = server_connections.all_connections(); + 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", { awayAnywhere: away_connections.length > 0, - away: typeof away_status === "string" ? true : !!away_status, + away: away_status, awayAll: connections.length === away_connections.length }); } - @EventHandler(["update_state_all", "update_state"]) - private updateStateMicrophone(event: Event) { + @EventHandler(["update_state_all", "update_state"]) + private updateStateMicrophone(event: Event) { if(event.type === "update_state") if(event.as<"update_state">().state !== "microphone") return; this.event_registry.fire("update_microphone_state", { - enabled: !!this.connection?.client_status.input_hardware, - muted: this.connection?.client_status.input_muted + enabled: !this.connection?.isMicrophoneDisabled(), + muted: !!this.connection?.isMicrophoneMuted() }); } - @EventHandler(["update_state_all", "update_state"]) - private updateStateSpeaker(event: Event) { + @EventHandler(["update_state_all", "update_state"]) + private updateStateSpeaker(event: Event) { if(event.type === "update_state") if(event.as<"update_state">().state !== "speaker") return; this.event_registry.fire("update_speaker_state", { - muted: this.connection?.client_status.output_muted + muted: !!this.connection?.isSpeakerMuted() }); } - @EventHandler(["update_state_all", "update_state"]) - private updateStateQuery(event: Event) { + @EventHandler(["update_state_all", "update_state"]) + private updateStateQuery(event: Event) { if(event.type === "update_state") if(event.as<"update_state">().state !== "query") return; this.event_registry.fire("update_query_state", { - queryShown: !!this.connection?.client_status.queries_visible + queryShown: !!this.connection?.areQueriesShown() }); } - @EventHandler(["update_state_all", "update_state"]) - private updateStateBookmarks(event: Event) { + @EventHandler(["update_state_all", "update_state"]) + private updateStateBookmarks(event: Event) { if(event.type === "update_state") if(event.as<"update_state">().state !== "bookmarks") return; @@ -549,7 +595,19 @@ export function control_bar_instance() : ControlBar | undefined { return react_reference_?.current; } +export type ControlStateUpdateType = "host-button" | "bookmarks" | "subscribe-mode" | "connect-state" | "away" | "microphone" | "speaker" | "query"; 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_host_button: HostButtonState; update_subscribe_state: ChannelSubscribeState; @@ -559,20 +617,131 @@ export interface ControlBarEvents { update_speaker_state: SpeakerState; update_query_state: QueryState; update_bookmarks: {}, - update_state: { - state: "host-button" | "bookmarks" | "subscribe-mode" | "connect-state" | "away" | "microphone" | "speaker" | "query" - }, update_state_all: { }, - /* trigger actions */ - set_connection_handler: { - handler?: ConnectionHandler + + /* UI-Actions */ + 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: { - handler: ConnectionHandler, - category: "audio" | "settings-initialized" | "connection-state" | "away-status" | "hostbanner" - } + action_toggle_query: { + shown: boolean + }, - //settings-initialized: Update query and channel flags + 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) { + 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.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; + } + }) } \ No newline at end of file diff --git a/shared/js/ui/htmltags.ts b/shared/js/ui/htmltags.ts index c890ef48..ef6b0dff 100644 --- a/shared/js/ui/htmltags.ts +++ b/shared/js/ui/htmltags.ts @@ -139,7 +139,7 @@ export namespace callbacks { 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(!client && client_id) { client = current_connection.channelTree.findClient(client_id); @@ -175,7 +175,7 @@ export namespace callbacks { export function callback_context_channel(element: JQuery) { 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; if(current_connection && current_connection.channelTree) { channel = current_connection.channelTree.findChannel(channel_id); diff --git a/shared/js/ui/modal/ModalConnect.ts b/shared/js/ui/modal/ModalConnect.ts index ba5f9ff1..c149d016 100644 --- a/shared/js/ui/modal/ModalConnect.ts +++ b/shared/js/ui/modal/ModalConnect.ts @@ -241,7 +241,7 @@ export function spawnConnectModal(options: { button_connect.on('click', event => { modal.close(); - const connection = server_connections.active_connection_handler(); + const connection = server_connections.active_connection(); if(connection) { connection.startConnection( 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 => { modal.close(); - const connection = server_connections.spawn_server_connection_handler(); - server_connections.set_active_connection_handler(connection); + const connection = server_connections.spawn_server_connection(); + server_connections.set_active_connection(connection); connection.startConnection( current_connect_data ? current_connect_data.address.hostname + ":" + current_connect_data.address.port : server_address(), selected_profile, diff --git a/shared/js/ui/modal/ModalSettings.ts b/shared/js/ui/modal/ModalSettings.ts index db3a39b4..56289045 100644 --- a/shared/js/ui/modal/ModalSettings.ts +++ b/shared/js/ui/modal/ModalSettings.ts @@ -92,7 +92,7 @@ function settings_general_application(container: JQuery, modal: Modal) { const option = container.find(".option-hostbanner-background") as JQuery; option.on('change', event => { 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(); }).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)); } - 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.channel_conversations().update_input_format_helper(); }); diff --git a/web/js/connection/ServerConnection.ts b/web/js/connection/ServerConnection.ts index 9b7b3469..39413afe 100644 --- a/web/js/connection/ServerConnection.ts +++ b/web/js/connection/ServerConnection.ts @@ -2,7 +2,8 @@ import { AbstractServerConnection, CommandOptionDefaults, CommandOptions, - ConnectionStateListener, voice + ConnectionStateListener, + voice } from "tc-shared/connection/ConnectionBase"; import {ConnectionHandler, ConnectionState, DisconnectReason} from "tc-shared/ConnectionHandler"; 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 {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import {settings, Settings} from "tc-shared/settings"; -import {LogCategory} 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 AbstractVoiceConnection = voice.AbstractVoiceConnection; import {AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler"; import * as elog from "tc-shared/ui/frames/server_log"; import {VoiceConnection} from "../voice/VoiceHandler"; +import AbstractVoiceConnection = voice.AbstractVoiceConnection; class ReturnListener { resolve: (value?: T | PromiseLike) => void; @@ -304,36 +305,38 @@ export class ServerConnection extends AbstractServerConnection { } async disconnect(reason?: string) : Promise { - clearTimeout(this._connect_timeout_timer); - this._connect_timeout_timer = undefined; + this.updateConnectionState(ConnectionState.DISCONNECTING); + try { + clearTimeout(this._connect_timeout_timer); + this._connect_timeout_timer = undefined; - clearTimeout(this._ping.thread_id); - this._ping.thread_id = undefined; + clearTimeout(this._ping.thread_id); + this._ping.thread_id = undefined; - if(typeof(reason) === "string") { - //TODO send disconnect reason - } + if(typeof(reason) === "string") { + //TODO send disconnect reason + } - if(this._connectionState != ConnectionState.UNCONNECTED) + if(this._voice_connection) + this._voice_connection.drop_rtp_session(); + + + if(this._socket_connected) { + this._socket_connected.close(3000 + 0xFF, tr("request disconnect")); + this._socket_connected = undefined; + } + + + for(let future of this._retListener) + future.reject(tr("Connection closed")); + this._retListener = []; + + this._connected = false; + this._retCodeIdx = 0; + } finally { this.updateConnectionState(ConnectionState.UNCONNECTED); - - if(this._voice_connection) - this._voice_connection.drop_rtp_session(); - - - if(this._socket_connected) { - this._socket_connected.close(3000 + 0xFF, tr("request disconnect")); - this._socket_connected = undefined; } - - - for(let future of this._retListener) - future.reject(tr("Connection closed")); - this._retListener = []; - - this._connected = false; - this._retCodeIdx = 0; } private handle_socket_message(data) {