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:
* **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

View File

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

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 {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:<br>"), 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();

View File

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

View File

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

View File

@ -110,6 +110,7 @@ export class Registry<Events> {
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(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<Events> {
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<Events> {
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];

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 {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<ClientGlobalControlEvents>) {
{
let microphone_muted = undefined;
@ -38,102 +39,16 @@ function initialize_sounds(event_registry: Registry<ClientGlobalControlEvents>)
}
}
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<ClientGlobalControlEvents>) {
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<ClientGlobalControlEvents>) {
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<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 {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
}
}
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 {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?<br>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<ClientGlobalControlEvents>;
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<ClientGlobalControlEvents>();
cmanager.initialize();
global_ev_handler.initialize(global_client_actions);
{
const bar = (
<cbar.ControlBar ref={cbar.react_reference()} multiSession={true} />
);
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;
}
}

View File

@ -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<boolean> = {
key: 'mute_input'
/* Default client states */
static readonly KEY_CLIENT_STATE_MICROPHONE_MUTED: SettingsKey<boolean> = {
key: 'client_state_microphone_muted',
default_value: false,
fallback_keys: ["mute_input"]
};
static readonly KEY_CONTROL_MUTE_OUTPUT: SettingsKey<boolean> = {
key: 'mute_output'
static readonly KEY_CLIENT_STATE_SPEAKER_MUTED: SettingsKey<boolean> = {
key: 'client_state_speaker_muted',
default_value: false,
fallback_keys: ["mute_output"]
};
static readonly KEY_CONTROL_SHOW_QUERIES: SettingsKey<boolean> = {
key: 'show_server_queries'
static readonly KEY_CLIENT_STATE_QUERY_SHOWN: SettingsKey<boolean> = {
key: 'client_state_query_shown',
default_value: false,
fallback_keys: ["show_server_queries"]
};
static readonly KEY_CONTROL_CHANNEL_SUBSCRIBE_ALL: SettingsKey<boolean> = {
key: 'channel_subscribe_all'
static readonly KEY_CLIENT_STATE_SUBSCRIBE_ALL_CHANNELS: SettingsKey<boolean> = {
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 */
@ -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<T>(key: string | SettingsKey<T>, 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);

View File

@ -12,6 +12,8 @@ export abstract class ReactComponentBase<Properties, State> 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));
}

View File

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

View File

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

View File

@ -78,7 +78,7 @@ export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
}
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 });

View File

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

View File

@ -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<ControlBarEvents> }, ConnectionState> {
class ConnectButton extends ReactComponentBase<{ multiSession: boolean; event_registry: Registry<InternalControlBarEvents> }, 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(
<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 {
subentries.push(
<DropdownEntry key={"disconnect-current"} icon={"client-disconnect"} text={<Translatable message={"Disconnect from current server"} />}
onClick={ () => client_control_events.fire("action_disconnect", { globally: false }) }/>
<DropdownEntry key={"disconnect-current-a"} icon={"client-disconnect"} text={<Translatable message={"Disconnect from current server"} />}
onClick={ () => this.props.event_registry.fire("action_disconnect", { globally: false }) }/>
);
}
if(this.state.connectedAnywhere) {
subentries.push(
<DropdownEntry key={"disconnect-current"} icon={"client-disconnect"} text={<Translatable message={"Disconnect from all servers"} />}
onClick={ () => client_control_events.fire("action_disconnect", { globally: true }) }/>
<DropdownEntry key={"disconnect-current-b"} icon={"client-disconnect"} text={<Translatable message={"Disconnect from all servers"} />}
onClick={ () => this.props.event_registry.fire("action_disconnect", { globally: true }) }/>
);
}
subentries.push(
<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) {
return (
<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}
</Button>
);
} else {
return (
<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}
</Button>
);
}
}
@EventHandler<ControlBarEvents>("update_connect_state")
@EventHandler<InternalControlBarEvents>("update_connect_state")
private handleStateUpdate(state: ConnectionState) {
this.updateState(state);
}
}
@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>;
protected initialize() {
@ -105,8 +107,9 @@ class BookmarkButton extends ReactComponentBase<{ event_registry: Registry<Contr
return (
<Button ref={this.button_ref} dropdownButtonExtraClass={cssButtonStyle.buttonBookmarks} autoSwitch={false} iconNormal={"client-bookmark_manager"}>
<DropdownEntry icon={"client-bookmark_manager"} text={<Translatable message={"Manage bookmarks"} />}
onClick={() => client_control_events.fire("action_open_window", { window: "bookmark-manage" })} />
<DropdownEntry icon={"client-bookmark_add"} text={<Translatable message={"Add current server to bookmarks"} />} />
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"} />}
onClick={() => this.props.event_registry.fire("action_add_current_server_to_bookmarks")} />
{marks}
</Button>
)
@ -160,7 +163,7 @@ class BookmarkButton extends ReactComponentBase<{ event_registry: Registry<Contr
}));
}
@EventHandler<ControlBarEvents>("update_bookmarks")
@EventHandler<InternalControlBarEvents>("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<ControlBarEvents> }, AwayState> {
class AwayButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, AwayState> {
protected default_state(): AwayState {
return {
away: false,
@ -186,35 +189,42 @@ class AwayButton extends ReactComponentBase<{ event_registry: Registry<ControlBa
let dropdowns = [];
if(this.state.away) {
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 {
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"} />}
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"} />);
if(this.state.awayAnywhere) {
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) {
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"} />}
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 (
<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}
</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) {
this.updateState(state);
}
@ -225,17 +235,17 @@ export interface ChannelSubscribeState {
}
@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 {
return { subscribeEnabled: false };
}
render() {
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) {
this.updateState(state);
}
@ -247,7 +257,7 @@ export interface MicrophoneState {
}
@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 {
return {
enabled: false,
@ -258,15 +268,15 @@ class MicrophoneButton extends ReactComponentBase<{ event_registry: Registry<Con
render() {
if(!this.state.enabled)
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)
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")}
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) {
this.updateState(state);
}
@ -277,7 +287,7 @@ export interface SpeakerState {
}
@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 {
return {
muted: false
@ -287,12 +297,12 @@ class SpeakerButton extends ReactComponentBase<{ event_registry: Registry<Contro
render() {
if(this.state.muted)
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")}
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) {
this.updateState(state);
}
@ -303,7 +313,7 @@ export interface QueryState {
}
@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() {
return {
queryShown: false
@ -313,22 +323,22 @@ class QueryButton extends ReactComponentBase<{ event_registry: Registry<ControlB
render() {
let toggle;
if(this.state.queryShown)
toggle = <DropdownEntry icon={""} text={<Translatable message={"Hide server queries"} />}
onClick={() => client_control_events.fire("action_toggle_query", { shown: false })}/>;
toggle = <DropdownEntry icon={"client-toggle_server_query_clients"} text={<Translatable message={"Hide server queries"} />}
onClick={() => this.props.event_registry.fire("action_toggle_query", { shown: false })}/>;
else
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 (
<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}
<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>
)
}
@EventHandler<ControlBarEvents>("update_query_state")
@EventHandler<InternalControlBarEvents>("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<ControlBarEvents> }, HostButtonState> {
class HostButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, HostButtonState> {
protected default_state() {
return {
url: undefined,
@ -370,7 +380,7 @@ class HostButton extends ReactComponentBase<{ event_registry: Registry<ControlBa
event.preventDefault();
}
@EventHandler<ControlBarEvents>("update_host_button")
@EventHandler<InternalControlBarEvents>("update_host_button")
private handleStateUpdate(state: HostButtonState) {
this.updateState(state);
}
@ -382,33 +392,25 @@ export interface ControlBarProperties {
@ReactEventHandler<ControlBar>(obj => obj.event_registry)
export class ControlBar extends React.Component<ControlBarProperties, {}> {
private readonly event_registry: Registry<ControlBarEvents>;
private readonly event_registry: Registry<InternalControlBarEvents>;
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<ControlBarEvents>();
this.event_registry = new Registry<InternalControlBarEvents>();
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<ControlBarEvents> { return this.event_registry; }
events() : Registry<InternalControlBarEvents> { return this.event_registry; }
render() {
return (
@ -428,22 +430,66 @@ export class ControlBar extends React.Component<ControlBarProperties, {}> {
)
}
@EventHandler<ControlBarEvents>("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<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateHostButton(event: Event<ControlBarEvents>) {
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<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;
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<ControlBarProperties, {}> {
}
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<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateSubscribe(event: Event<ControlBarEvents>) {
@EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateSubscribe(event: Event<InternalControlBarEvents>) {
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<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateConnect(event: Event<ControlBarEvents>) {
@EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateConnect(event: Event<InternalControlBarEvents>) {
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<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateAway(event: Event<ControlBarEvents>) {
@EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateAway(event: Event<InternalControlBarEvents>) {
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<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateMicrophone(event: Event<ControlBarEvents>) {
@EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateMicrophone(event: Event<InternalControlBarEvents>) {
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<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateSpeaker(event: Event<ControlBarEvents>) {
@EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateSpeaker(event: Event<InternalControlBarEvents>) {
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<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateQuery(event: Event<ControlBarEvents>) {
@EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateQuery(event: Event<InternalControlBarEvents>) {
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<ControlBarEvents>(["update_state_all", "update_state"])
private updateStateBookmarks(event: Event<ControlBarEvents>) {
@EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateBookmarks(event: Event<InternalControlBarEvents>) {
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
},
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
}
}
//settings-initialized: Update query and channel flags
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.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;
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);

View File

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

View File

@ -92,7 +92,7 @@ function settings_general_application(container: JQuery, modal: Modal) {
const option = container.find(".option-hostbanner-background") as JQuery<HTMLInputElement>;
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();
});

View File

@ -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<T> {
resolve: (value?: T | PromiseLike<T>) => void;
@ -304,6 +305,8 @@ export class ServerConnection extends AbstractServerConnection {
}
async disconnect(reason?: string) : Promise<void> {
this.updateConnectionState(ConnectionState.DISCONNECTING);
try {
clearTimeout(this._connect_timeout_timer);
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)
this._voice_connection.drop_rtp_session();
@ -334,6 +334,9 @@ export class ServerConnection extends AbstractServerConnection {
this._connected = false;
this._retCodeIdx = 0;
} finally {
this.updateConnectionState(ConnectionState.UNCONNECTED);
}
}
private handle_socket_message(data) {