import * as React from "react"; import {Button} from "./button"; import {DropdownEntry} from "tc-shared/ui/frames/control-bar/dropdown"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase"; import { ConnectionEvents, ConnectionHandler, ConnectionStateUpdateType } from "tc-shared/ConnectionHandler"; import {Event, EventHandler, ReactEventHandler, Registry} from "tc-shared/events"; import {Settings, settings} from "tc-shared/settings"; import { add_server_to_bookmarks, Bookmark, bookmarks, BookmarkType, boorkmak_connect, DirectoryBookmark, find_bookmark } from "tc-shared/bookmarks"; import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; import {createInputModal} from "tc-shared/ui/elements/Modal"; import {global_client_actions} from "tc-shared/events/GlobalEvents"; import {ConnectionManagerEvents, server_connections} from "tc-shared/ConnectionManager"; const cssStyle = require("./index.scss"); const cssButtonStyle = require("./button.scss"); export interface ConnectionState { connected: boolean; connectedAnywhere: boolean; } @ReactEventHandler(obj => obj.props.event_registry) class ConnectButton extends ReactComponentBase<{ multiSession: boolean; event_registry: Registry }, ConnectionState> { protected defaultState(): ConnectionState { return { connected: false, connectedAnywhere: false } } render() { let subentries = []; if(this.props.multiSession) { if(!this.state.connected) { subentries.push( Connect to a server} onClick={ () => global_client_actions.fire("action_open_window_connect", {newTab: false }) } /> ); } else { subentries.push( Disconnect from current server} onClick={ () => this.props.event_registry.fire("action_disconnect", { globally: false }) }/> ); } if(this.state.connectedAnywhere) { subentries.push( Disconnect from all servers} onClick={ () => this.props.event_registry.fire("action_disconnect", { globally: true }) }/> ); } subentries.push( Connect to a server in another tab} onClick={ () => global_client_actions.fire("action_open_window_connect", { newTab: true }) } /> ); } if(!this.state.connected) { return ( ); } else { return ( ); } } @EventHandler("update_connect_state") private handleStateUpdate(state: ConnectionState) { this.setState(state); } } @ReactEventHandler(obj => obj.props.event_registry) class BookmarkButton extends ReactComponentBase<{ event_registry: Registry }, {}> { private button_ref: React.RefObject ) } private renderBookmark(bookmark: Bookmark) { return ( ); } private renderDirectory(directory: DirectoryBookmark) { return ( {directory.content.map(e => e.type === BookmarkType.DIRECTORY ? this.renderDirectory(e) : this.renderBookmark(e))} ) } private static onBookmarkClick(bookmark_id: string) { const bookmark = find_bookmark(bookmark_id) as Bookmark; if(!bookmark) return; boorkmak_connect(bookmark, false); } private onBookmarkContextMenu(bookmark_id: string, event: MouseEvent) { event.preventDefault(); const bookmark = find_bookmark(bookmark_id) as Bookmark; if(!bookmark) return; this.button_ref.current?.setState({ dropdownForceShow: true }); contextmenu.spawn_context_menu(event.pageX, event.pageY, { type: contextmenu.MenuEntryType.ENTRY, name: tr("Connect"), icon_class: 'client-connect', callback: () => boorkmak_connect(bookmark, false) }, { type: contextmenu.MenuEntryType.ENTRY, name: tr("Connect in a new tab"), icon_class: 'client-connect', callback: () => boorkmak_connect(bookmark, true), visible: !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION) }, contextmenu.Entry.CLOSE(() => { this.button_ref.current?.setState({ dropdownForceShow: false }); })); } @EventHandler("update_bookmarks") private handleStateUpdate() { this.forceUpdate(); } } export interface AwayState { away: boolean; awayAnywhere: boolean; awayAll: boolean; } @ReactEventHandler(obj => obj.props.event_registry) class AwayButton extends ReactComponentBase<{ event_registry: Registry }, AwayState> { protected defaultState(): AwayState { return { away: false, awayAnywhere: false, awayAll: false }; } render() { let dropdowns = []; if(this.state.away) { dropdowns.push(Go online} onClick={() => this.props.event_registry.fire("action_disable_away", { globally: false })} />); } else { dropdowns.push(Set away on this server} onClick={() => this.props.event_registry.fire("action_set_away", { globally: false, prompt_reason: false })} />); } dropdowns.push(Set away message on this server} onClick={() => this.props.event_registry.fire("action_set_away", { globally: false, prompt_reason: true })} />); dropdowns.push(
); if(this.state.awayAnywhere) { dropdowns.push(Go online for all servers} onClick={() => this.props.event_registry.fire("action_disable_away", { globally: true })} />); } if(!this.state.awayAll) { dropdowns.push(Set away on all servers} onClick={() => this.props.event_registry.fire("action_set_away", { globally: true, prompt_reason: false })} />); } dropdowns.push(Set away message for all servers} onClick={() => this.props.event_registry.fire("action_set_away", { globally: true, prompt_reason: true })} />); /* switchable because we're switching it manually */ return ( ); } 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.setState(state); } } export interface ChannelSubscribeState { subscribeEnabled: boolean; } @ReactEventHandler(obj => obj.props.event_registry) class ChannelSubscribeButton extends ReactComponentBase<{ event_registry: Registry }, ChannelSubscribeState> { protected defaultState(): ChannelSubscribeState { return { subscribeEnabled: false }; } render() { return ) } @EventHandler("update_query_state") private handleStateUpdate(state: QueryState) { this.setState(state); } } export interface HostButtonState { url?: string; title?: string; target_url?: string; } @ReactEventHandler(obj => obj.props.event_registry) class HostButton extends ReactComponentBase<{ event_registry: Registry }, HostButtonState> { protected defaultState() { return { url: undefined, target_url: undefined }; } render() { if(!this.state.url) return null; return ( {tr("Hostbutton")} ); } private onClick(event: MouseEvent) { window.open(this.state.target_url || this.state.url, '_blank'); event.preventDefault(); } @EventHandler("update_host_button") private handleStateUpdate(state: HostButtonState) { this.setState(state); } } export interface ControlBarProperties { multiSession: boolean; } @ReactEventHandler(obj => obj.event_registry) export class ControlBar extends React.Component { 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.enableDebug("control-bar"); initialize(this.event_registry); } events() : Registry { return this.event_registry; } render() { return (
) } private handleActiveConnectionHandlerChanged(event: ConnectionManagerEvents["notify_active_handler_changed"]) { if(event.oldHandler) this.unregisterConnectionHandlerEvents(event.oldHandler); this.connection = event.newHandler; if(event.newHandler) this.registerConnectionHandlerEvents(event.newHandler); this.event_registry.fire("set_connection_handler", { handler: this.connection }); this.event_registry.fire("update_state_all"); } 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); //FIXME: Add the host button here! } 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 { 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 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, title: undefined }); return; } this.event_registry.fire("update_host_button", { 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) { if(event.type === "update_state") if(event.as<"update_state">().state !== "subscribe-mode") return; this.event_registry.fire("update_subscribe_state", { subscribeEnabled: !!this.connection?.isSubscribeToAllChannels() }); } @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.all_connections().findIndex(e => e.connected) !== -1, connected: !!this.connection?.connected }); } @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.all_connections(); const away_connections = server_connections.all_connections().filter(e => e.isAway()); const away_status = !!this.connection?.isAway(); this.event_registry.fire("update_away_state", { awayAnywhere: away_connections.length > 0, away: away_status, awayAll: connections.length === away_connections.length }); } @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?.isMicrophoneDisabled(), muted: !!this.connection?.isMicrophoneMuted() }); } @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?.isSpeakerMuted() }); } @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?.areQueriesShown() }); } @EventHandler(["update_state_all", "update_state"]) private updateStateBookmarks(event: Event) { if(event.type === "update_state") if(event.as<"update_state">().state !== "bookmarks") return; this.event_registry.fire("update_bookmarks"); } } let react_reference_: React.RefObject; export function react_reference() { return react_reference_ || (react_reference_ = React.createRef()); } 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; update_connect_state: ConnectionState; update_away_state: AwayState; update_microphone_state: MicrophoneState; update_speaker_state: SpeakerState; update_query_state: QueryState; update_bookmarks: {}, update_state_all: { }, /* 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; }, 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 } } 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); current_connection_handler.acquireInputHardware().then(() => {}); } }); 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; } }) }