From cbd20a21771105f331d14c6405bbded946eb057e Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Mon, 28 Sep 2020 09:37:48 +0200 Subject: [PATCH] Implemented the channel popout ui --- ChangeLog.md | 5 +- shared/js/ConnectionHandler.ts | 1 - shared/js/bookmarks.ts | 13 +- shared/js/events.ts | 8 +- shared/js/main.tsx | 13 +- shared/js/tree/Server.ts | 8 - shared/js/ui/context-menu/ReactRenderer.tsx | 8 +- shared/js/ui/frames/MenuBar.ts | 2 - .../control-bar/{button.scss => Button.scss} | 33 + .../control-bar/{button.tsx => Button.tsx} | 28 +- shared/js/ui/frames/control-bar/Controller.ts | 340 ++++++++ .../js/ui/frames/control-bar/Definitions.ts | 44 ++ shared/js/ui/frames/control-bar/DropDown.tsx | 56 ++ .../control-bar/{index.scss => Renderer.scss} | 8 + shared/js/ui/frames/control-bar/Renderer.tsx | 371 +++++++++ shared/js/ui/frames/control-bar/dropdown.tsx | 60 -- shared/js/ui/frames/control-bar/index.tsx | 748 ------------------ .../ui/frames/side/PopoutConversationUI.tsx | 6 +- shared/js/ui/modal/ModalBookmarks.ts | 2 - shared/js/ui/modal/css-editor/Controller.ts | 2 +- shared/js/ui/modal/css-editor/Renderer.tsx | 6 +- .../external-modal/Controller.ts | 9 +- .../external-modal/IPCMessage.ts | 36 +- .../external-modal/PopoutController.ts | 24 +- .../external-modal/PopoutEntrypoint.ts | 6 +- .../external-modal/PopoutRegistry.ts | 2 +- .../ui/react-elements/external-modal/index.ts | 8 +- shared/js/ui/tree/Controller.tsx | 19 +- shared/js/ui/tree/RendererModal.tsx | 28 - shared/js/ui/tree/popout/Controller.ts | 41 + shared/js/ui/tree/popout/RendererModal.scss | 47 ++ shared/js/ui/tree/popout/RendererModal.tsx | 43 + shared/js/video-viewer/Controller.ts | 2 +- shared/js/video-viewer/Renderer.tsx | 6 +- web/app/ExternalModalFactory.ts | 12 +- web/app/hooks/ExternalModal.ts | 2 +- 36 files changed, 1113 insertions(+), 934 deletions(-) rename shared/js/ui/frames/control-bar/{button.scss => Button.scss} (92%) rename shared/js/ui/frames/control-bar/{button.tsx => Button.tsx} (68%) create mode 100644 shared/js/ui/frames/control-bar/Controller.ts create mode 100644 shared/js/ui/frames/control-bar/Definitions.ts create mode 100644 shared/js/ui/frames/control-bar/DropDown.tsx rename shared/js/ui/frames/control-bar/{index.scss => Renderer.scss} (82%) create mode 100644 shared/js/ui/frames/control-bar/Renderer.tsx delete mode 100644 shared/js/ui/frames/control-bar/dropdown.tsx delete mode 100644 shared/js/ui/frames/control-bar/index.tsx delete mode 100644 shared/js/ui/tree/RendererModal.tsx create mode 100644 shared/js/ui/tree/popout/Controller.ts create mode 100644 shared/js/ui/tree/popout/RendererModal.scss create mode 100644 shared/js/ui/tree/popout/RendererModal.tsx diff --git a/ChangeLog.md b/ChangeLog.md index 0543fc68..2f3f13cf 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,8 @@ # Changelog: -* **16.09.20** +* **27.09.20** + - Middle clicking on bookmarks now directly connects in a new tab + +* **26.09.20** - Updating group prefix/suffixes when the group naming mode changes - Added a client talk power indicator - Fixed channel info description not rendering diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 48570a7d..39f1f0ca 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -37,7 +37,6 @@ import {ServerFeature, ServerFeatures} from "./connection/ServerFeatures"; import {ChannelTree} from "./tree/ChannelTree"; import {LocalClientEntry} from "./tree/Client"; import {ServerAddress} from "./tree/Server"; -import {server_connections} from "tc-shared/ConnectionManager"; export enum InputHardwareState { MISSING, diff --git a/shared/js/bookmarks.ts b/shared/js/bookmarks.ts index 8971d2dd..32b960ac 100644 --- a/shared/js/bookmarks.ts +++ b/shared/js/bookmarks.ts @@ -5,9 +5,16 @@ import {createErrorModal, createInfoModal, createInputModal} from "./ui/elements import {defaultConnectProfile, findConnectProfile} from "./profiles/ConnectionProfile"; import {spawnConnectModal} from "./ui/modal/ModalConnect"; import * as top_menu from "./ui/frames/MenuBar"; -import {control_bar_instance} from "./ui/frames/control-bar"; import {ConnectionHandler} from "./ConnectionHandler"; import {server_connections} from "tc-shared/ConnectionManager"; +import {Registry} from "tc-shared/events"; + +/* TODO: much better events? */ +export interface BookmarkEvents { + notify_bookmarks_updated: {} +} + +export const bookmarkEvents = new Registry(); export const boorkmak_connect = (mark: Bookmark, new_tab?: boolean) => { const profile = findConnectProfile(mark.connect_profile) || defaultConnectProfile(); @@ -217,6 +224,7 @@ export function change_directory(parent: DirectoryBookmark, bookmark: Bookmark | } export function save_bookmark(bookmark?: Bookmark | DirectoryBookmark) { + bookmarkEvents.fire("notify_bookmarks_updated"); save_config(); /* nvm we dont give a fuck... saving everything */ } @@ -249,10 +257,7 @@ export function add_server_to_bookmarks(server: ConnectionHandler) { }, name); save_bookmark(bookmark); - 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(); } }).open(); diff --git a/shared/js/events.ts b/shared/js/events.ts index a0dbccbc..0092bd5d 100644 --- a/shared/js/events.ts +++ b/shared/js/events.ts @@ -4,6 +4,7 @@ import {guid} from "./crypto/uid"; import * as React from "react"; import {useEffect} from "react"; import {unstable_batchedUpdates} from "react-dom"; +import {ext} from "twemoji"; export interface Event { readonly type: T; @@ -217,9 +218,10 @@ export class Registry { - let index = callbacks.length; - while(index--) { + let index = 0; + while(index < callbacks.length) { this.fire(callbacks[index].type, callbacks[index].data); + index++; } }); } @@ -287,6 +289,8 @@ export class Registry(events: (keyof EventTypes) | (keyof EventTypes)[]) { return function (target: any, propertyKey: string, diff --git a/shared/js/main.tsx b/shared/js/main.tsx index b7046fb1..b54f5b99 100644 --- a/shared/js/main.tsx +++ b/shared/js/main.tsx @@ -22,7 +22,6 @@ import * as ppt from "tc-backend/ppt"; import * as keycontrol from "./KeyControl"; 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 {global_client_actions} from "tc-shared/events/GlobalEvents"; import {FileTransferState, TransferProvider,} from "tc-shared/file/Transfer"; @@ -49,6 +48,10 @@ import {defaultConnectProfile, findConnectProfile} from "tc-shared/profiles/Conn import {server_connections} from "tc-shared/ConnectionManager"; import {initializeConnectionUIList} from "tc-shared/ui/frames/connection-handler-list/Controller"; import ContextMenuEvent = JQuery.ContextMenuEvent; +import {Registry} from "tc-shared/events"; +import {ControlBarEvents} from "tc-shared/ui/frames/control-bar/Definitions"; +import {ControlBar2} from "tc-shared/ui/frames/control-bar/Renderer"; +import {initializeControlBarController} from "tc-shared/ui/frames/control-bar/Controller"; let preventWelcomeUI = false; async function initialize() { @@ -68,11 +71,9 @@ async function initialize_app() { global_ev_handler.initialize(global_client_actions); { - const bar = ( - - ); - - ReactDOM.render(bar, $(".container-control-bar")[0]); + const events = new Registry() + initializeControlBarController(events, "main"); + ReactDOM.render(, $(".container-control-bar")[0]); } /* diff --git a/shared/js/tree/Server.ts b/shared/js/tree/Server.ts index 747be4db..e88f7ea9 100644 --- a/shared/js/tree/Server.ts +++ b/shared/js/tree/Server.ts @@ -12,7 +12,6 @@ import {spawnIconSelect} from "../ui/modal/ModalIconSelect"; import {spawnAvatarList} from "../ui/modal/ModalAvatarList"; import {connection_log} from "../ui/modal/ModalConnect"; import * as top_menu from "../ui/frames/MenuBar"; -import {control_bar_instance} from "../ui/frames/control-bar"; import {Registry} from "../events"; import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry"; @@ -294,8 +293,6 @@ export class ServerEntry extends ChannelTreeEntry { update_bookmarks = true; } else if(variable.key.indexOf('hostbanner') != -1) { update_bannner = true; - } else if(variable.key.indexOf('hostbutton') != -1) { - update_button = true; } } { @@ -315,17 +312,12 @@ export class ServerEntry extends ChannelTreeEntry { }); bookmarks.save_bookmark(); top_menu.rebuild_bookmarks(); - - control_bar_instance()?.events().fire("update_state", { state: "bookmarks" }); } } if(update_bannner) this.channelTree.client.hostbanner.update(); - if(update_button) - control_bar_instance()?.events().fire("server_updated", { handler: this.channelTree.client, category: "hostbanner" }); - group.end(); if(is_self_notify && this.info_request_promise_resolve) { this.info_request_promise_resolve(); diff --git a/shared/js/ui/context-menu/ReactRenderer.tsx b/shared/js/ui/context-menu/ReactRenderer.tsx index 7ed0bce4..ea947ee7 100644 --- a/shared/js/ui/context-menu/ReactRenderer.tsx +++ b/shared/js/ui/context-menu/ReactRenderer.tsx @@ -168,17 +168,21 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { ReactDOM.render(, globalContainer); reactContextMenuInstance = new class implements ContextMenuFactory { - spawnContextMenu(position: { pageX: number; pageY: number }, entries: ContextMenuEntry[]) { + spawnContextMenu(position: { pageX: number; pageY: number }, entries: ContextMenuEntry[], closeCallback: () => void) { entries.forEach(generateUniqueIds); refRenderer.current?.setState({ entries: entries, pageX: position.pageX, - pageY: position.pageY + pageY: position.pageY, + callbackClose: closeCallback }); } closeContextMenu() { if(refRenderer.current?.state.entries?.length) { + const callback = refRenderer.current?.state.callbackClose; + if(callback) { callback(); } + refRenderer.current?.setState({ callbackClose: undefined, entries: [] }); } } diff --git a/shared/js/ui/frames/MenuBar.ts b/shared/js/ui/frames/MenuBar.ts index 32b56e7c..48e55e26 100644 --- a/shared/js/ui/frames/MenuBar.ts +++ b/shared/js/ui/frames/MenuBar.ts @@ -19,7 +19,6 @@ import {spawnQueryCreate} from "../../ui/modal/ModalQuery"; import {spawnAbout} from "../../ui/modal/ModalAbout"; import * as loader from "tc-loader"; import {formatMessage} from "../../ui/frames/chat"; -import {control_bar_instance} from "../../ui/frames/control-bar"; import {spawnPermissionEditorModal} from "../../ui/modal/permission/ModalPermissionEditor"; import {global_client_actions} from "tc-shared/events/GlobalEvents"; import {server_connections} from "tc-shared/ConnectionManager"; @@ -346,7 +345,6 @@ export function initialize() { for(const handler of handlers) handler.disconnectFromServer(); - control_bar_instance()?.events().fire("update_state", { state: "connect-state" }); update_state(); }; item = menu.append_item(tr("Disconnect from current server")); diff --git a/shared/js/ui/frames/control-bar/button.scss b/shared/js/ui/frames/control-bar/Button.scss similarity index 92% rename from shared/js/ui/frames/control-bar/button.scss rename to shared/js/ui/frames/control-bar/Button.scss index b7b79d24..fe0293bc 100644 --- a/shared/js/ui/frames/control-bar/button.scss +++ b/shared/js/ui/frames/control-bar/Button.scss @@ -73,6 +73,9 @@ html:root { flex-direction: row; justify-content: center; + flex-grow: 0; + flex-shrink: 0; + height: 2em; width: 2em; @@ -272,4 +275,34 @@ html:root { .dropdown { width: 300px; } +} + +.arrow { + display: inline-block; + border: solid black; + + border-width: 0 .2em .2em 0; + padding: .21em; + height: .5em; + width: .5em; + + &.right { + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); + } + + &.left { + transform: rotate(135deg); + -webkit-transform: rotate(135deg); + } + + &.up { + transform: rotate(-135deg); + -webkit-transform: rotate(-135deg); + } + + &.down { + transform: rotate(45deg); + -webkit-transform: rotate(45deg); + } } \ No newline at end of file diff --git a/shared/js/ui/frames/control-bar/button.tsx b/shared/js/ui/frames/control-bar/Button.tsx similarity index 68% rename from shared/js/ui/frames/control-bar/button.tsx rename to shared/js/ui/frames/control-bar/Button.tsx index 21878ce2..da343771 100644 --- a/shared/js/ui/frames/control-bar/button.tsx +++ b/shared/js/ui/frames/control-bar/Button.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase"; -import {DropdownContainer} from "tc-shared/ui/frames/control-bar/dropdown"; -const cssStyle = require("./button.scss"); +import {DropdownContainer} from "./DropDown"; +const cssStyle = require("./Button.scss"); export interface ButtonState { switched: boolean; @@ -21,7 +21,7 @@ export interface ButtonProperties { onToggle?: (state: boolean) => boolean | void; - dropdownButtonExtraClass?: string; + className?: string; switched?: boolean; } @@ -41,21 +41,23 @@ export class Button extends ReactComponentBase { cssStyle.button, switched ? cssStyle.activated : "", typeof this.props.colorTheme === "string" ? cssStyle["theme-" + this.props.colorTheme] : ""); - const button = ( -
-
-
- ); - if(!this.hasChildren()) - return button; + if(!this.hasChildren()) { + return ( +
+
+
+ ); + } return ( -
+
- {button} +
+
+
-
+
diff --git a/shared/js/ui/frames/control-bar/Controller.ts b/shared/js/ui/frames/control-bar/Controller.ts new file mode 100644 index 00000000..fa7ee092 --- /dev/null +++ b/shared/js/ui/frames/control-bar/Controller.ts @@ -0,0 +1,340 @@ +import {Registry} from "tc-shared/events"; +import { + Bookmark, + ControlBarEvents, + ControlBarMode, + HostButtonInfo +} from "tc-shared/ui/frames/control-bar/Definitions"; +import {server_connections} from "tc-shared/ConnectionManager"; +import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler"; +import {Settings, settings} from "tc-shared/settings"; +import {global_client_actions} from "tc-shared/events/GlobalEvents"; +import { + add_server_to_bookmarks, + Bookmark as ServerBookmark, bookmarkEvents, + bookmarks, + bookmarks_flat, + BookmarkType, + boorkmak_connect, + DirectoryBookmark +} from "tc-shared/bookmarks"; +import {LogCategory, logWarn} from "tc-shared/log"; +import {createInputModal} from "tc-shared/ui/elements/Modal"; + +class InfoController { + private readonly mode: ControlBarMode; + private readonly events: Registry; + private currentHandler: ConnectionHandler; + + private globalEvents: (() => void)[] = []; + private globalHandlerRegisteredEvents: {[key: string]: (() => void)[]} = {}; + private handlerRegisteredEvents: (() => void)[] = []; + + constructor(events: Registry, mode: ControlBarMode) { + this.events = events; + this.mode = mode; + } + + public getCurrentHandler() : ConnectionHandler { return this.currentHandler; } + public getMode() : ControlBarMode { return this.mode; } + + public initialize() { + server_connections.all_connections().forEach(handler => this.registerGlobalHandlerEvents(handler)); + + const events = this.globalEvents; + events.push(server_connections.events().on("notify_handler_created", event => { + this.registerGlobalHandlerEvents(event.handler); + this.sendConnectionState(); + this.sendAwayState(); + })); + events.push(server_connections.events().on("notify_handler_deleted", event => { + this.unregisterGlobalHandlerEvents(event.handler); + this.sendConnectionState(); + this.sendAwayState(); + })); + events.push(bookmarkEvents.on("notify_bookmarks_updated", () => this.sendBookmarks())); + + if(this.mode === "main") { + events.push(server_connections.events().on("notify_active_handler_changed", event => this.setConnectionHandler(event.newHandler))); + } + + this.setConnectionHandler(server_connections.active_connection()); + } + + public destroy() { + server_connections.all_connections().forEach(handler => this.unregisterGlobalHandlerEvents(handler)); + this.unregisterCurrentHandlerEvents(); + + this.globalEvents.forEach(callback => callback()); + this.globalEvents = []; + } + + private registerGlobalHandlerEvents(handler: ConnectionHandler) { + const events = this.globalHandlerRegisteredEvents[handler.handlerId] = []; + + events.push(handler.events().on("notify_connection_state_changed", () => this.sendConnectionState())); + events.push(handler.events().on("notify_state_updated", event => { + if(event.state === "away") { this.sendAwayState(); } + })); + } + + private unregisterGlobalHandlerEvents(handler: ConnectionHandler) { + const callbacks = this.globalHandlerRegisteredEvents[handler.handlerId]; + if(!callbacks) { return; } + + delete this.globalHandlerRegisteredEvents[handler.handlerId]; + callbacks.forEach(callback => callback()); + } + + private registerCurrentHandlerEvents(handler: ConnectionHandler) { + const events = this.handlerRegisteredEvents; + + events.push(handler.events().on("notify_connection_state_changed", event => { + if(event.old_state === ConnectionState.CONNECTED || event.new_state === ConnectionState.CONNECTED) { + this.sendHostButton(); + } + })); + + events.push(handler.channelTree.server.events.on("notify_properties_updated", event => { + if("virtualserver_hostbutton_gfx_url" in event.updated_properties || + "virtualserver_hostbutton_url" in event.updated_properties || + "virtualserver_hostbutton_tooltip" in event.updated_properties) { + this.sendHostButton(); + } + })); + + events.push(handler.events().on("notify_state_updated", event => { + if(event.state === "microphone") { + this.sendMicrophoneState(); + } else if(event.state === "speaker") { + this.sendSpeakerState(); + } else if(event.state === "query") { + this.sendQueryState(); + } else if(event.state === "subscribe") { + this.sendSubscribeState(); + } + })); + } + + private unregisterCurrentHandlerEvents() { + this.handlerRegisteredEvents.forEach(callback => callback()); + this.handlerRegisteredEvents = []; + } + + + public setConnectionHandler(handler: ConnectionHandler) { + if(handler === this.currentHandler) { return; } + + this.currentHandler = handler; + this.unregisterCurrentHandlerEvents(); + this.registerCurrentHandlerEvents(handler); + + /* update all states */ + this.sendConnectionState(); + this.sendBookmarks(); /* not really required, not directly related to the connection handler */ + this.sendAwayState(); + this.sendMicrophoneState(); + this.sendSpeakerState(); + this.sendSubscribeState(); + this.sendQueryState(); + this.sendHostButton(); + } + + public sendConnectionState() { + const globallyConnected = server_connections.all_connections().findIndex(e => e.connected) !== -1; + const locallyConnected = this.currentHandler?.connected; + const multisession = !settings.static_global(Settings.KEY_DISABLE_MULTI_SESSION); + + this.events.fire_async("notify_connection_state", { + state: { + currentlyConnected: locallyConnected, + generallyConnected: globallyConnected, + multisession: multisession + } + }); + } + + public sendBookmarks() { + const buildInfo = (bookmark: DirectoryBookmark | ServerBookmark) => { + if(bookmark.type === BookmarkType.DIRECTORY) { + return { + uniqueId: bookmark.unique_id, + label: bookmark.display_name, + children: bookmark.content.map(buildInfo) + } as Bookmark; + } else { + return { + uniqueId: bookmark.unique_id, + label: bookmark.display_name, + icon: bookmark.last_icon_id ? { iconId: bookmark.last_icon_id, serverUniqueId: bookmark.last_icon_server_id } : undefined + } as Bookmark; + } + }; + + this.events.fire_async("notify_bookmarks", { + marks: bookmarks().content.map(buildInfo) + }); + } + + public sendAwayState() { + const globalAwayCount = server_connections.all_connections().filter(handler => handler.isAway()).length; + const awayLocally = !!this.currentHandler?.isAway(); + + this.events.fire_async("notify_away_state", { + state: { + globallyAway: globalAwayCount === server_connections.all_connections().length ? "full" : globalAwayCount > 0 ? "partial" : "none", + locallyAway: awayLocally + } + }); + } + + public sendMicrophoneState() { + this.events.fire_async("notify_microphone_state", { + state: this.currentHandler?.isMicrophoneDisabled() ? "disabled" : this.currentHandler?.isMicrophoneMuted() ? "muted" : "enabled" + }); + } + + public sendSpeakerState() { + this.events.fire_async("notify_speaker_state", { + enabled: !this.currentHandler?.isSpeakerMuted() + }); + } + + public sendSubscribeState() { + this.events.fire_async("notify_subscribe_state", { + subscribe: !!this.currentHandler?.isSubscribeToAllChannels() + }); + } + + public sendQueryState() { + this.events.fire_async("notify_query_state", { + shown: !!this.currentHandler?.areQueriesShown() + }); + } + + public sendHostButton() { + let info: HostButtonInfo; + + if(this.currentHandler?.connected) { + const properties = this.currentHandler.channelTree.server.properties; + info = properties.virtualserver_hostbutton_gfx_url ? { + url: properties.virtualserver_hostbutton_gfx_url, + target: properties.virtualserver_hostbutton_url, + title: properties.virtualserver_hostbutton_tooltip + } : undefined; + } + + this.events.fire_async("notify_host_button", { + button: info + }); + } +} + +export function initializePopoutControlBarController(events: Registry, handler: ConnectionHandler) { + const infoHandler = initializeControlBarController(events, "channel-popout"); + infoHandler.setConnectionHandler(handler); +} + +export function initializeClientControlBarController(events: Registry) { + initializeControlBarController(events, "main"); +} + +export function initializeControlBarController(events: Registry, mode: ControlBarMode) : InfoController { + const infoHandler = new InfoController(events, mode); + infoHandler.initialize(); + + events.on("notify_destroy", () => infoHandler.destroy()); + + events.on("query_mode", () => events.fire_async("notify_mode", { mode: infoHandler.getMode() })); + events.on("query_connection_state", () => infoHandler.sendConnectionState()); + events.on("query_bookmarks", () => infoHandler.sendBookmarks()); + events.on("query_away_state", () => infoHandler.sendAwayState()); + events.on("query_microphone_state", () => infoHandler.sendMicrophoneState()); + events.on("query_speaker_state", () => infoHandler.sendSpeakerState()); + events.on("query_subscribe_state", () => infoHandler.sendSubscribeState()); + events.on("query_host_button", () => infoHandler.sendHostButton()); + + events.on("action_connection_connect", event => global_client_actions.fire("action_open_window_connect", { newTab: event.newTab })); + events.on("action_connection_disconnect", event => { + (event.generally ? server_connections.all_connections() : [infoHandler.getCurrentHandler()]).filter(e => !!e).forEach(connection => { + connection.disconnectFromServer().then(() => {}); + }); + }); + + events.on("action_bookmark_manage", () => global_client_actions.fire("action_open_window", { window: "bookmark-manage" })); + events.on("action_bookmark_add_current_server", () => add_server_to_bookmarks(infoHandler.getCurrentHandler())); + events.on("action_bookmark_connect", event => { + const bookmark = bookmarks_flat().find(mark => mark.unique_id === event.bookmarkUniqueId); + if(!bookmark) { + logWarn(LogCategory.BOOKMARKS, tr("Tried to connect to a non existing bookmark with id %s"), event.bookmarkUniqueId); + return; + } + + boorkmak_connect(bookmark, event.newTab); + }); + + events.on("action_toggle_away", event => { + if(event.away) { + const setAway = 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.promptMessage) { + createInputModal(tr("Set away message"), tr("Please enter your away message"), () => true, message => { + if(typeof(message) === "string") + setAway(message); + }).open(); + } else { + setAway(undefined); + } + } else { + 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); + } + }); + + events.on("action_toggle_microphone", event => { + /* change the default global setting */ + settings.changeGlobal(Settings.KEY_CLIENT_STATE_MICROPHONE_MUTED, !event.enabled); + + const current_connection_handler = infoHandler.getCurrentHandler(); + if(current_connection_handler) { + current_connection_handler.setMicrophoneMuted(!event.enabled); + current_connection_handler.acquireInputHardware().then(() => {}); + } + }); + + events.on("action_toggle_speaker", event => { + /* change the default global setting */ + settings.changeGlobal(Settings.KEY_CLIENT_STATE_SPEAKER_MUTED, !event.enabled); + + infoHandler.getCurrentHandler()?.setSpeakerMuted(!event.enabled); + }); + + events.on("action_toggle_subscribe", event => { + settings.changeGlobal(Settings.KEY_CLIENT_STATE_SUBSCRIBE_ALL_CHANNELS, event.subscribe); + + infoHandler.getCurrentHandler()?.setSubscribeToAllChannels(event.subscribe); + }); + + events.on("action_toggle_query", event => { + settings.changeGlobal(Settings.KEY_CLIENT_STATE_QUERY_SHOWN, event.show); + + infoHandler.getCurrentHandler()?.setQueriesShown(event.show); + }); + events.on("action_query_manage", () => { + global_client_actions.fire("action_open_window", { window: "query-manage" }); + }); + + return infoHandler; +} \ No newline at end of file diff --git a/shared/js/ui/frames/control-bar/Definitions.ts b/shared/js/ui/frames/control-bar/Definitions.ts new file mode 100644 index 00000000..43392599 --- /dev/null +++ b/shared/js/ui/frames/control-bar/Definitions.ts @@ -0,0 +1,44 @@ +import {RemoteIconInfo} from "tc-shared/file/Icons"; + +export type ControlBarMode = "main" | "channel-popout"; +export type ConnectionState = { currentlyConnected: boolean, generallyConnected: boolean, multisession: boolean }; +export type Bookmark = { uniqueId: string, label: string, icon: RemoteIconInfo | undefined, children?: Bookmark[] }; +export type AwayState = { locallyAway: boolean, globallyAway: "partial" | "full" | "none" }; +export type MicrophoneState = "enabled" | "disabled" | "muted"; +export type HostButtonInfo = { title?: string, target?: string, url: string }; + +export interface ControlBarEvents { + action_connection_connect: { newTab: boolean }, + action_connection_disconnect: { generally: boolean }, + action_bookmark_connect: { bookmarkUniqueId: string, newTab: boolean }, + action_bookmark_manage: {}, + action_bookmark_add_current_server: {}, + action_toggle_away: { away: boolean, globally: boolean, promptMessage?: boolean }, + action_toggle_microphone: { enabled: boolean }, + action_toggle_speaker: { enabled: boolean }, + action_toggle_subscribe: { subscribe: boolean }, + action_toggle_query: { show: boolean }, + action_query_manage: {}, + + query_mode: {}, + query_connection_state: {}, + query_bookmarks: {}, + query_away_state: {}, + query_microphone_state: {}, + query_speaker_state: {}, + query_subscribe_state: {}, + query_query_state: {}, + query_host_button: {}, + + notify_mode: { mode: ControlBarMode } + notify_connection_state: { state: ConnectionState }, + notify_bookmarks: { marks: Bookmark[] }, + notify_away_state: { state: AwayState }, + notify_microphone_state: { state: MicrophoneState }, + notify_speaker_state: { enabled: boolean }, + notify_subscribe_state: { subscribe: boolean }, + notify_query_state: { shown: boolean }, + notify_host_button: { button: HostButtonInfo | undefined }, + + notify_destroy: {} +} \ No newline at end of file diff --git a/shared/js/ui/frames/control-bar/DropDown.tsx b/shared/js/ui/frames/control-bar/DropDown.tsx new file mode 100644 index 00000000..d8f763f1 --- /dev/null +++ b/shared/js/ui/frames/control-bar/DropDown.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase"; +import {IconRenderer, RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon"; +import {getIconManager, RemoteIconInfo} from "tc-shared/file/Icons"; +const cssStyle = require("./Button.scss"); + +export interface DropdownEntryProperties { + icon?: string | RemoteIconInfo; + text: JSX.Element | string; + + onClick?: (event: React.MouseEvent) => void; + onAuxClick?: (event: React.MouseEvent) => void; + onContextMenu?: (event: React.MouseEvent) => void; + + children?: React.ReactElement[] +} + +const LocalIconRenderer = (props: { icon?: string | RemoteIconInfo }) => { + if(!props.icon || typeof props.icon === "string") { + return + } else { + return ; + } +} + +export class DropdownEntry extends ReactComponentBase { + protected defaultState() { return {}; } + + render() { + if(this.props.children) { + return ( +
+ + {this.props.text} +
+ + {this.props.children} + +
+ ); + } else { + return ( + + ); + } + } +} + +export const DropdownContainer = (props: { children: any }) => ( +
+ {props.children} +
+); \ No newline at end of file diff --git a/shared/js/ui/frames/control-bar/index.scss b/shared/js/ui/frames/control-bar/Renderer.scss similarity index 82% rename from shared/js/ui/frames/control-bar/index.scss rename to shared/js/ui/frames/control-bar/Renderer.scss index 420d9e58..108f8ab0 100644 --- a/shared/js/ui/frames/control-bar/index.scss +++ b/shared/js/ui/frames/control-bar/Renderer.scss @@ -37,4 +37,12 @@ html:root { min-width: 0; } +} + +@media all and (max-width: 300px) { + .controlBar.mode-channel-popout { + .hideSmallPopout { + display: none!important; + } + } } \ No newline at end of file diff --git a/shared/js/ui/frames/control-bar/Renderer.tsx b/shared/js/ui/frames/control-bar/Renderer.tsx new file mode 100644 index 00000000..92edee75 --- /dev/null +++ b/shared/js/ui/frames/control-bar/Renderer.tsx @@ -0,0 +1,371 @@ +import {Registry} from "tc-shared/events"; +import { + AwayState, + Bookmark, + ControlBarEvents, + ConnectionState, + ControlBarMode, HostButtonInfo, MicrophoneState +} from "tc-shared/ui/frames/control-bar/Definitions"; +import * as React from "react"; +import {useContext, useRef, useState} from "react"; +import {DropdownEntry} from "tc-shared/ui/frames/control-bar/DropDown"; +import {Translatable} from "tc-shared/ui/react-elements/i18n"; +import {Button} from "tc-shared/ui/frames/control-bar/Button"; +import {spawnContextMenu} from "tc-shared/ui/context-menu"; +import {ClientIcon} from "svg-sprites/client-icons"; + +const cssStyle = require("./Renderer.scss"); +const cssButtonStyle = require("./Button.scss"); + +const Events = React.createContext>(undefined); +const ModeContext = React.createContext(undefined); + +const ConnectButton = () => { + const events = useContext(Events); + + const [ state, setState ] = useState(() => { + events.fire("query_connection_state"); + return undefined; + }); + + events.reactUse("notify_connection_state", event => setState(event.state)); + + let subentries = []; + if(state?.multisession) { + if(!state.currentlyConnected) { + subentries.push( + Connect to a server} + onClick={() => events.fire("action_connection_connect", { newTab: false })} /> + ); + } else { + subentries.push( + Disconnect from current server} + onClick={() => events.fire("action_connection_disconnect", { generally: false })} /> + ); + } + if(state.generallyConnected) { + subentries.push( + Disconnect from all servers} + onClick={() => events.fire("action_connection_disconnect", { generally: true })}/> + ); + } + subentries.push( + Connect to a server in another tab} + onClick={() => events.fire("action_connection_connect", { newTab: true })} /> + ); + } + + if(state?.currentlyConnected) { + return ( + + ); + } else { + return ( + + ); + } +}; + +const BookmarkRenderer = (props: { bookmark: Bookmark, refButton: React.RefObject + ) +}; + +const AwayButton = () => { + const events = useContext(Events); + + const [ state, setState ] = useState(() => { + events.fire("query_away_state"); + return undefined; + }); + + events.on("notify_away_state", event => setState(event.state)); + + let dropdowns = []; + if(state?.locallyAway) { + dropdowns.push(Go online} + onClick={() => events.fire("action_toggle_away", { away: false, globally: false })} />); + } else { + dropdowns.push(Set away on this server} + onClick={() => events.fire("action_toggle_away", { away: true, globally: false })} />); + } + dropdowns.push(Set away message on this server} + onClick={() => events.fire("action_toggle_away", { away: true, globally: false, promptMessage: true })} />); + + dropdowns.push(
); + if(state?.globallyAway !== "none") { + dropdowns.push(Go online for all servers} + onClick={() => events.fire("action_toggle_away", { away: false, globally: true })} />); + } + if(state?.globallyAway !== "full") { + dropdowns.push(Set away on all servers} + onClick={() => events.fire("action_toggle_away", { away: true, globally: true })} />); + } + dropdowns.push(Set away message for all servers} + onClick={() => events.fire("action_toggle_away", { away: true, globally: true, promptMessage: true })} />); + + return ( + + ); +}; + +const MicrophoneButton = () => { + const events = useContext(Events); + + const [ state, setState ] = useState(() => { + events.fire("query_microphone_state"); + return undefined; + }); + + events.on("notify_microphone_state", event => setState(event.state)); + + if(state === "muted") { + return + ); + } +}; + +const HostButton = () => { + const events = useContext(Events); + + const [ hostButton, setHostButton ] = useState(() => { + events.fire("query_host_button"); + return undefined; + }); + + events.reactUse("notify_host_button", event => setHostButton(event.button)); + + if(!hostButton) { + return null; + } else { + return ( + { + window.open(hostButton.target || hostButton.url, '_blank'); + event.preventDefault(); + }} + > + {tr("Hostbutton")} + + ); + } +}; + +export const ControlBar2 = (props: { events: Registry, className?: string }) => { + const [ mode, setMode ] = useState(() => { + props.events.fire("query_mode"); + return undefined; + }); + + props.events.reactUse("notify_mode", event => setMode(event.mode)); + + const items = []; + + if(mode !== "channel-popout") { + items.push(); + } + items.push(); + items.push(
); + items.push(); + items.push(); + items.push(); + items.push(
); + items.push(); + items.push(); + items.push(
); + items.push(); + + return ( + + +
+ {items} +
+
+
+ ) +}; \ No newline at end of file diff --git a/shared/js/ui/frames/control-bar/dropdown.tsx b/shared/js/ui/frames/control-bar/dropdown.tsx deleted file mode 100644 index 5c9c7515..00000000 --- a/shared/js/ui/frames/control-bar/dropdown.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from "react"; -import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase"; -import {IconRenderer, RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon"; -import {getIconManager, RemoteIconInfo} from "tc-shared/file/Icons"; -const cssStyle = require("./button.scss"); - -export interface DropdownEntryProperties { - icon?: string | RemoteIconInfo; - text: JSX.Element | string; - - onClick?: (event) => void; - onContextMenu?: (event) => void; -} - -export class DropdownEntry extends ReactComponentBase { - protected defaultState() { return {}; } - - render() { - if(this.props.children) { - return ( -
- {typeof this.props.icon === "string" ? : - - } - {this.props.text} -
- - {this.props.children} - -
- ); - } else { - return ( -
- {typeof this.props.icon === "string" ? : - - } - {this.props.text} -
- ); - } - } -} - -export interface DropdownContainerProperties { } -export interface DropdownContainerState { } - -export class DropdownContainer extends ReactComponentBase { - protected defaultState() { - return { }; - } - - render() { - return ( -
- {this.props.children} -
- ); - } -} diff --git a/shared/js/ui/frames/control-bar/index.tsx b/shared/js/ui/frames/control-bar/index.tsx deleted file mode 100644 index 409f2803..00000000 --- a/shared/js/ui/frames/control-bar/index.tsx +++ /dev/null @@ -1,748 +0,0 @@ -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; - } - }) -} \ No newline at end of file diff --git a/shared/js/ui/frames/side/PopoutConversationUI.tsx b/shared/js/ui/frames/side/PopoutConversationUI.tsx index ad15a3cc..d793ef79 100644 --- a/shared/js/ui/frames/side/PopoutConversationUI.tsx +++ b/shared/js/ui/frames/side/PopoutConversationUI.tsx @@ -1,4 +1,4 @@ -import {Registry} from "tc-shared/events"; +import {Registry, RegistryMap} from "tc-shared/events"; import {ConversationUIEvents} from "tc-shared/ui/frames/side/ConversationDefinitions"; import {ConversationPanel} from "tc-shared/ui/frames/side/ConversationUI"; import * as React from "react"; @@ -8,11 +8,11 @@ class PopoutConversationUI extends AbstractModal { private readonly events: Registry; private readonly userData: any; - constructor(events: Registry, userData: any) { + constructor(registryMap: RegistryMap, userData: any) { super(); this.userData = userData; - this.events = events; + this.events = registryMap["default"] as any; } renderBody() { diff --git a/shared/js/ui/modal/ModalBookmarks.ts b/shared/js/ui/modal/ModalBookmarks.ts index da46923c..37859dbd 100644 --- a/shared/js/ui/modal/ModalBookmarks.ts +++ b/shared/js/ui/modal/ModalBookmarks.ts @@ -19,7 +19,6 @@ import {LogCategory} from "../../log"; import * as i18nc from "../../i18n/country"; import {formatMessage} from "../../ui/frames/chat"; import * as top_menu from "../frames/MenuBar"; -import {control_bar_instance} from "../../ui/frames/control-bar"; import {generateIconJQueryTag, getIconManager} from "tc-shared/file/Icons"; export function spawnBookmarkModal() { @@ -404,7 +403,6 @@ export function spawnBookmarkModal() { modal.htmlTag.dividerfy().find(".modal-body").addClass("modal-bookmarks"); modal.close_listener.push(() => { - control_bar_instance()?.events().fire("update_state", {state: "bookmarks"}); top_menu.rebuild_bookmarks(); }); diff --git a/shared/js/ui/modal/css-editor/Controller.ts b/shared/js/ui/modal/css-editor/Controller.ts index 761d60d1..75d86ae3 100644 --- a/shared/js/ui/modal/css-editor/Controller.ts +++ b/shared/js/ui/modal/css-editor/Controller.ts @@ -171,7 +171,7 @@ export function spawnModalCssVariableEditor() { const events = new Registry(); cssVariableEditorController(events); - const modal = spawnExternalModal("css-editor", events, {}); + const modal = spawnExternalModal("css-editor", { default: events }, {}); modal.show(); } diff --git a/shared/js/ui/modal/css-editor/Renderer.tsx b/shared/js/ui/modal/css-editor/Renderer.tsx index 030007e0..d3c1d32a 100644 --- a/shared/js/ui/modal/css-editor/Renderer.tsx +++ b/shared/js/ui/modal/css-editor/Renderer.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import {useState} from "react"; import {CssEditorEvents, CssEditorUserData, CssVariable} from "tc-shared/ui/modal/css-editor/Definitions"; -import {Registry} from "tc-shared/events"; +import {Registry, RegistryMap} from "tc-shared/events"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {BoxedInputField, FlatInputField} from "tc-shared/ui/react-elements/InputField"; import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; @@ -393,11 +393,11 @@ class PopoutConversationUI extends AbstractModal { private readonly events: Registry; private readonly userData: CssEditorUserData; - constructor(events: Registry, userData: CssEditorUserData) { + constructor(registryMap: RegistryMap, userData: CssEditorUserData) { super(); this.userData = userData; - this.events = events; + this.events = registryMap["default"] as any; this.events.on("notify_export_result", event => { createInfoModal(tr("Config exported successfully"), tr("The config has been exported successfully.")).open(); diff --git a/shared/js/ui/react-elements/external-modal/Controller.ts b/shared/js/ui/react-elements/external-modal/Controller.ts index d2e64a68..0dc4af25 100644 --- a/shared/js/ui/react-elements/external-modal/Controller.ts +++ b/shared/js/ui/react-elements/external-modal/Controller.ts @@ -2,7 +2,7 @@ import * as log from "../../../log"; import {LogCategory} from "../../../log"; import * as ipc from "../../../ipc/BrowserIPC"; import {ChannelMessage} from "../../../ipc/BrowserIPC"; -import {Registry} from "../../../events"; +import {Registry, RegistryMap} from "../../../events"; import { EventControllerBase, Popout2ControllerMessages, @@ -20,8 +20,9 @@ export abstract class AbstractExternalModalController extends EventControllerBas private readonly documentUnloadListener: () => void; private callbackWindowInitialized: (error?: string) => void; - protected constructor(modal: string, localEventRegistry: Registry, userData: any) { - super(localEventRegistry); + protected constructor(modal: string, registries: RegistryMap, userData: any) { + super(); + this.initializeRegistries(registries); this.modalEvents = new Registry(); @@ -154,7 +155,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas this.callbackWindowInitialized = undefined; } - this.sendIPCMessage("hello-controller", { accepted: true, userData: this.userData }); + this.sendIPCMessage("hello-controller", { accepted: true, userData: this.userData, registries: Object.keys(this.localRegistries) }); break; } diff --git a/shared/js/ui/react-elements/external-modal/IPCMessage.ts b/shared/js/ui/react-elements/external-modal/IPCMessage.ts index a6c3e9ac..310abcf3 100644 --- a/shared/js/ui/react-elements/external-modal/IPCMessage.ts +++ b/shared/js/ui/react-elements/external-modal/IPCMessage.ts @@ -1,14 +1,15 @@ import {ChannelMessage, IPCChannel} from "../../../ipc/BrowserIPC"; -import {EventReceiver, Registry} from "../../../events"; +import {EventReceiver, RegistryMap} from "../../../events"; export interface PopoutIPCMessage { "hello-popout": { version: string }, - "hello-controller": { accepted: boolean, message?: string, userData?: any }, + "hello-controller": { accepted: boolean, message?: string, userData?: any, registries?: string[] }, "fire-event": { type: string; payload: any; callbackId: string; + registry: string; }, "fire-event-callback": { @@ -38,30 +39,42 @@ export abstract class EventControllerBase protected ipcChannel: IPCChannel; protected ipcRemoteId: string; - protected readonly localEventRegistry: Registry; - private readonly localEventReceiver: EventReceiver; + protected localRegistries: RegistryMap; + private localEventReceiver: {[key: string]: EventReceiver}; private omitEventType: string = undefined; private omitEventData: any; private eventFiredListeners: {[key: string]:{ callback: () => void, timeout: number }} = {}; - protected constructor(localEventRegistry: Registry) { - this.localEventRegistry = localEventRegistry; + protected constructor() { } + protected initializeRegistries(registries: RegistryMap) { + if(typeof this.localRegistries !== "undefined") { throw "event registries have already been initialized" }; + + this.localEventReceiver = {}; + this.localRegistries = registries; + + for(const key of Object.keys(this.localRegistries)) { + this.localEventReceiver[key] = this.createEventReceiver(key); + this.localRegistries[key].connectAll(this.localEventReceiver[key]); + } + } + + private createEventReceiver(key: string) : EventReceiver { let refThis = this; - this.localEventReceiver = new class implements EventReceiver { + return new class implements EventReceiver { fire(eventType: T, data?: any[T], overrideTypeKey?: boolean) { if(refThis.omitEventType === eventType && refThis.omitEventData === data) { refThis.omitEventType = undefined; return; } - refThis.sendIPCMessage("fire-event", { type: eventType, payload: data, callbackId: undefined }); + refThis.sendIPCMessage("fire-event", { type: eventType, payload: data, callbackId: undefined, registry: key }); } fire_async(eventType: T, data?: any[T], callback?: () => void) { const callbackId = callback ? (++callbackIdIndex) + "-ev-cb" : undefined; - refThis.sendIPCMessage("fire-event", { type: eventType, payload: data, callbackId: callbackId }); + refThis.sendIPCMessage("fire-event", { type: eventType, payload: data, callbackId: callbackId, registry: key }); if(callbackId) { const timeout = setTimeout(() => { delete refThis.eventFiredListeners[callbackId]; @@ -75,7 +88,6 @@ export abstract class EventControllerBase } } }; - this.localEventRegistry.connectAll(this.localEventReceiver); } protected handleIPCMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) { @@ -97,7 +109,7 @@ export abstract class EventControllerBase const tpayload = payload as PopoutIPCMessage["fire-event"]; this.omitEventData = tpayload.payload; this.omitEventType = tpayload.type; - this.localEventRegistry.fire(tpayload.type as any, tpayload.payload); + this.localRegistries[tpayload.registry].fire(tpayload.type as any, tpayload.payload); if(tpayload.callbackId) this.sendIPCMessage("fire-event-callback", { callbackId: tpayload.callbackId }); break; @@ -117,7 +129,7 @@ export abstract class EventControllerBase } protected destroyIPC() { - this.localEventRegistry.disconnectAll(this.localEventReceiver); + Object.keys(this.localRegistries).forEach(key => this.localRegistries[key].disconnectAll(this.localEventReceiver[key])); this.ipcChannel = undefined; this.ipcRemoteId = undefined; this.eventFiredListeners = {}; diff --git a/shared/js/ui/react-elements/external-modal/PopoutController.ts b/shared/js/ui/react-elements/external-modal/PopoutController.ts index 2472fd4a..ce393a99 100644 --- a/shared/js/ui/react-elements/external-modal/PopoutController.ts +++ b/shared/js/ui/react-elements/external-modal/PopoutController.ts @@ -4,7 +4,7 @@ import { Controller2PopoutMessages, EventControllerBase, PopoutIPCMessage } from "../../../ui/react-elements/external-modal/IPCMessage"; -import {Registry} from "../../../events"; +import {Registry, RegistryMap} from "../../../events"; const kSettingIPCChannel: SettingsKey = { key: "ipc-channel", @@ -24,16 +24,14 @@ class PopoutController extends EventControllerBase<"popout"> { private callbackControllerHello: (accepted: boolean | string) => void; constructor() { - super(new Registry()); + super(); this.ipcRemoteId = Settings.instance.static(Settings.KEY_IPC_REMOTE_ADDRESS, "invalid"); this.ipcChannel = getIPCInstance().createChannel(this.ipcRemoteId, Settings.instance.static(kSettingIPCChannel, "invalid")); this.ipcChannel.messageHandler = this.handleIPCMessage.bind(this); } - getEventRegistry() { - return this.localEventRegistry; - } + getEventRegistries() : RegistryMap { return this.localRegistries; } async initialize() { this.sendIPCMessage("hello-popout", { version: __build.version }); @@ -65,8 +63,22 @@ class PopoutController extends EventControllerBase<"popout"> { case "hello-controller": { const tpayload = payload as PopoutIPCMessage["hello-controller"]; console.log("Received Hello World from controller. Window instance accpected: %o", tpayload.accepted); - if(!this.callbackControllerHello) + if(!this.callbackControllerHello) { return; + } + + if(this.getEventRegistries()) { + const registries = this.getEventRegistries(); + const invalidIndex = tpayload.registries.findIndex(reg => !registries[reg]); + if(invalidIndex !== -1) { + console.error("Received miss matching event registry keys (missing %s)", tpayload.registries[invalidIndex]); + this.callbackControllerHello("miss matching registry keys (locally)"); + } + } else { + let map = {}; + tpayload.registries.forEach(reg => map[reg] = new Registry()); + this.initializeRegistries(map); + } this.userData = tpayload.userData; this.callbackControllerHello(tpayload.accepted ? true : tpayload.message || false); diff --git a/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts b/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts index abb9ea36..f96e6a30 100644 --- a/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts +++ b/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts @@ -7,7 +7,7 @@ import {AbstractModal, ModalRenderer} from "../../../ui/react-elements/ModalDefi import {Settings, SettingsKey} from "../../../settings"; import {getPopoutController} from "./PopoutController"; import {findPopoutHandler} from "../../../ui/react-elements/external-modal/PopoutRegistry"; -import {Registry} from "../../../events"; +import {RegistryMap} from "../../../events"; import {WebModalRenderer} from "../../../ui/react-elements/external-modal/PopoutRendererWeb"; import {ClientModalRenderer} from "../../../ui/react-elements/external-modal/PopoutRendererClient"; import {setupJSRender} from "../../../ui/jsrender"; @@ -18,7 +18,7 @@ import "../../context-menu"; let modalRenderer: ModalRenderer; let modalInstance: AbstractModal; -let modalClass: new (events: Registry, userData: any) => AbstractModal; +let modalClass: new (events: RegistryMap, userData: any) => AbstractModal; const kSettingModalTarget: SettingsKey = { key: "modal-target", @@ -92,7 +92,7 @@ loader.register_task(Stage.LOADED, { priority: 100, function: async () => { try { - modalInstance = new modalClass(getPopoutController().getEventRegistry(), getPopoutController().getUserData()); + modalInstance = new modalClass(getPopoutController().getEventRegistries(), getPopoutController().getUserData()); modalRenderer.renderModal(modalInstance); } catch(error) { loader.critical_error("Failed to invoker modal", "Lookup the console for more detail"); diff --git a/shared/js/ui/react-elements/external-modal/PopoutRegistry.ts b/shared/js/ui/react-elements/external-modal/PopoutRegistry.ts index 19cf3f54..29f66093 100644 --- a/shared/js/ui/react-elements/external-modal/PopoutRegistry.ts +++ b/shared/js/ui/react-elements/external-modal/PopoutRegistry.ts @@ -34,5 +34,5 @@ registerHandler({ registerHandler({ name: "channel-tree", - loadClass: async () => await import("tc-shared/ui/tree/RendererModal") + loadClass: async () => await import("tc-shared/ui/tree/popout/RendererModal") }); diff --git a/shared/js/ui/react-elements/external-modal/index.ts b/shared/js/ui/react-elements/external-modal/index.ts index c85be503..df12fd91 100644 --- a/shared/js/ui/react-elements/external-modal/index.ts +++ b/shared/js/ui/react-elements/external-modal/index.ts @@ -1,17 +1,17 @@ -import {Registry} from "../../../events"; +import {Registry, RegistryMap} from "../../../events"; import "./Controller"; import {ModalController} from "../../../ui/react-elements/ModalDefinitions"; /* we've to reference him here, else the client would not */ -export type ControllerFactory = (modal: string, events: Registry, userData: any) => ModalController; +export type ControllerFactory = (modal: string, registryMap: RegistryMap, userData: any, uniqueModalId: string) => ModalController; let modalControllerFactory: ControllerFactory; export function setExternalModalControllerFactory(factory: ControllerFactory) { modalControllerFactory = factory; } -export function spawnExternalModal(modal: string, events: Registry, userData: any) : ModalController { +export function spawnExternalModal(modal: string, registryMap: RegistryMap, userData: any, uniqueModalId?: string) : ModalController { if(typeof modalControllerFactory === "undefined") throw tr("No external modal factory has been set"); - return modalControllerFactory(modal, events as any, userData); + return modalControllerFactory(modal, registryMap, userData, uniqueModalId); } \ No newline at end of file diff --git a/shared/js/ui/tree/Controller.tsx b/shared/js/ui/tree/Controller.tsx index 5b1e1222..0aa2984b 100644 --- a/shared/js/ui/tree/Controller.tsx +++ b/shared/js/ui/tree/Controller.tsx @@ -19,25 +19,22 @@ import {VoiceConnectionEvents, VoiceConnectionStatus} from "tc-shared/connection import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTransfer"; import {GroupManager, GroupManagerEvents} from "tc-shared/permission/GroupManager"; import {ServerEntry} from "tc-shared/tree/Server"; -import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal"; +import {spawnChannelTreePopout} from "tc-shared/ui/tree/popout/Controller"; export function renderChannelTree(channelTree: ChannelTree, target: HTMLElement) { const events = new Registry(); events.enableDebug("channel-tree-view"); - initializeTreeController(events, channelTree); + initializeChannelTreeController(events, channelTree); - ReactDOM.render([ + ReactDOM.render( - // this.onMoveEnd(point.x, point.y)} ref={this.view_move} /> - ], target); + , target); + /* (window as any).chan_pop = () => { - const events = new Registry(); - events.enableDebug("channel-tree-view-modal"); - initializeTreeController(events, channelTree); - const modal = spawnExternalModal("channel-tree", events, { handlerId: channelTree.client.handlerId }); - modal.show(); + spawnChannelTreePopout(channelTree.client); } + */ } /* FIXME: Client move is not a part of the channel tree, it's part of our own controller here */ @@ -514,7 +511,7 @@ class ChannelTreeController { } } -function initializeTreeController(events: Registry, channelTree: ChannelTree) { +export function initializeChannelTreeController(events: Registry, channelTree: ChannelTree) { /* initialize the general update handler */ const controller = new ChannelTreeController(events, channelTree); controller.initialize(); diff --git a/shared/js/ui/tree/RendererModal.tsx b/shared/js/ui/tree/RendererModal.tsx deleted file mode 100644 index 5284bea8..00000000 --- a/shared/js/ui/tree/RendererModal.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions"; -import {Registry} from "tc-shared/events"; -import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions"; -import * as React from "react"; -import {Translatable} from "tc-shared/ui/react-elements/i18n"; -import {ChannelTreeRenderer} from "tc-shared/ui/tree/Renderer"; - -class ChannelTreeModal extends AbstractModal { - readonly events: Registry; - readonly handlerId: string; - - constructor(registry: Registry, userData: any) { - super(); - - this.handlerId = userData.handlerId; - this.events = registry; - } - - renderBody(): React.ReactElement { - return ; - } - - title(): string | React.ReactElement { - return Channel tree; - } -} - -export = ChannelTreeModal; \ No newline at end of file diff --git a/shared/js/ui/tree/popout/Controller.ts b/shared/js/ui/tree/popout/Controller.ts new file mode 100644 index 00000000..84634337 --- /dev/null +++ b/shared/js/ui/tree/popout/Controller.ts @@ -0,0 +1,41 @@ +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {Registry} from "tc-shared/events"; +import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions"; +import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal"; +import {initializeChannelTreeController} from "tc-shared/ui/tree/Controller"; +import {ControlBarEvents} from "tc-shared/ui/frames/control-bar/Definitions"; +import { + initializePopoutControlBarController +} from "tc-shared/ui/frames/control-bar/Controller"; +import {server_connections} from "tc-shared/ConnectionManager"; + +export function spawnChannelTreePopout(handler: ConnectionHandler) { + const eventsTree = new Registry(); + eventsTree.enableDebug("channel-tree-view-modal"); + initializeChannelTreeController(eventsTree, handler.channelTree); + + const eventsControlBar = new Registry(); + initializePopoutControlBarController(eventsControlBar, handler); + + let handlerDestroyListener; + server_connections.events().on("notify_handler_deleted", handlerDestroyListener = event => { + if(event.handler !== handler) { + return; + } + + modal.destroy(); + }); + + const modal = spawnExternalModal("channel-tree", { tree: eventsTree, controlBar: eventsControlBar }, { handlerId: handler.handlerId }, "channel-tree-" + handler.handlerId); + modal.show(); + + modal.getEvents().on("destroy", () => { + server_connections.events().off("notify_handler_deleted", handlerDestroyListener); + + eventsTree.fire("notify_destroy"); + eventsTree.destroy(); + + eventsControlBar.fire("notify_destroy"); + eventsControlBar.destroy(); + }); +} \ No newline at end of file diff --git a/shared/js/ui/tree/popout/RendererModal.scss b/shared/js/ui/tree/popout/RendererModal.scss new file mode 100644 index 00000000..c59ead6b --- /dev/null +++ b/shared/js/ui/tree/popout/RendererModal.scss @@ -0,0 +1,47 @@ +.container { + display: flex; + flex-direction: column; + justify-content: stretch; + + height: 100%; + width: 100%; + + background: #1e1e1e; + + padding: 5px; + + .containerControlBar { + z-index: 200; + + flex-shrink: 0; + border-radius: 5px; + + font-size: .75em; + + height: 2em; + width: 100%; + background-color: #454545; + + display: flex; + flex-direction: column; + justify-content: center; + + margin-bottom: 5px; + } + + .containerChannelTree { + height: 100%; + + background: #363535; + min-width: 200px; + + display: flex; + flex-direction: column; + justify-content: stretch; + + min-height: 100px; + overflow: hidden; + + border-radius: 5px; + } +} \ No newline at end of file diff --git a/shared/js/ui/tree/popout/RendererModal.tsx b/shared/js/ui/tree/popout/RendererModal.tsx new file mode 100644 index 00000000..ff3a5fce --- /dev/null +++ b/shared/js/ui/tree/popout/RendererModal.tsx @@ -0,0 +1,43 @@ +import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions"; +import {Registry, RegistryMap} from "tc-shared/events"; +import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions"; +import * as React from "react"; +import {Translatable} from "tc-shared/ui/react-elements/i18n"; +import {ChannelTreeRenderer} from "tc-shared/ui/tree/Renderer"; +import {ControlBarEvents} from "tc-shared/ui/frames/control-bar/Definitions"; +import {ControlBar2} from "tc-shared/ui/frames/control-bar/Renderer"; + +const cssStyle = require("./RendererModal.scss"); +class ChannelTreeModal extends AbstractModal { + readonly eventsTree: Registry; + readonly eventsControlBar: Registry; + + readonly handlerId: string; + + constructor(registryMap: RegistryMap, userData: any) { + super(); + + this.handlerId = userData.handlerId; + this.eventsTree = registryMap["tree"] as any; + this.eventsControlBar = registryMap["controlBar"] as any; + } + + renderBody(): React.ReactElement { + return ( +
+
+ +
+
+ +
+
+ ) + } + + title(): string | React.ReactElement { + return Channel tree; + } +} + +export = ChannelTreeModal; \ No newline at end of file diff --git a/shared/js/video-viewer/Controller.ts b/shared/js/video-viewer/Controller.ts index 928e197f..dcec40fd 100644 --- a/shared/js/video-viewer/Controller.ts +++ b/shared/js/video-viewer/Controller.ts @@ -40,7 +40,7 @@ class VideoViewer { throw tr("Missing video viewer plugin"); } - this.modal = spawnExternalModal("video-viewer", this.events, { handlerId: connection.handlerId }); + this.modal = spawnExternalModal("video-viewer", { default: this.events }, { handlerId: connection.handlerId }); this.registerPluginListeners(); this.plugin.getCurrentWatchers().forEach(watcher => this.registerWatcherEvents(watcher)); diff --git a/shared/js/video-viewer/Renderer.tsx b/shared/js/video-viewer/Renderer.tsx index 5223c9aa..a293cba3 100644 --- a/shared/js/video-viewer/Renderer.tsx +++ b/shared/js/video-viewer/Renderer.tsx @@ -3,7 +3,7 @@ import {LogCategory} from "tc-shared/log"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; import * as React from "react"; import {useEffect, useRef, useState} from "react"; -import {Registry} from "tc-shared/events"; +import {Registry, RegistryMap} from "tc-shared/events"; import {PlayerStatus, VideoViewerEvents} from "./Definitions"; import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; import ReactPlayer from 'react-player' @@ -494,11 +494,11 @@ class ModalVideoPopout extends AbstractModal { readonly events: Registry; readonly handlerId: string; - constructor(registry: Registry, userData: any) { + constructor(registryMap: RegistryMap, userData: any) { super(); this.handlerId = userData.handlerId; - this.events = registry; + this.events = registryMap["default"] as any; } title(): string | React.ReactElement { diff --git a/web/app/ExternalModalFactory.ts b/web/app/ExternalModalFactory.ts index 4d1aaa2b..69d5fd51 100644 --- a/web/app/ExternalModalFactory.ts +++ b/web/app/ExternalModalFactory.ts @@ -4,14 +4,17 @@ import * as ipc from "tc-shared/ipc/BrowserIPC"; import {ChannelMessage} from "tc-shared/ipc/BrowserIPC"; import {LogCategory, logDebug, logWarn} from "tc-shared/log"; import {Popout2ControllerMessages, PopoutIPCMessage} from "tc-shared/ui/react-elements/external-modal/IPCMessage"; +import {RegistryMap} from "tc-shared/events"; export class ExternalModalController extends AbstractExternalModalController { + private readonly uniqueModalId: string; private currentWindow: Window; private windowClosedTestInterval: number = 0; private windowClosedTimeout: number; - constructor(a, b, c) { - super(a, b, c); + constructor(modal: string, registries: RegistryMap, userData: any, uniqueModalId: string) { + super(modal, registries, userData); + this.uniqueModalId = uniqueModalId || modal; } protected async spawnWindow() : Promise { @@ -37,8 +40,9 @@ export class ExternalModalController extends AbstractExternalModalController { }); } - if(!this.currentWindow) + if(!this.currentWindow) { return false; + } this.currentWindow.onbeforeunload = () => { clearInterval(this.windowClosedTestInterval); @@ -101,7 +105,7 @@ export class ExternalModalController extends AbstractExternalModalController { let baseUrl = location.origin + location.pathname + "?"; return window.open( baseUrl + Object.keys(parameters).map(e => e + "=" + encodeURIComponent(parameters[e])).join("&"), - this.modalType, + this.uniqueModalId, Object.keys(features).map(e => e + "=" + features[e]).join(",") ); } diff --git a/web/app/hooks/ExternalModal.ts b/web/app/hooks/ExternalModal.ts index cb2ef546..5504f55d 100644 --- a/web/app/hooks/ExternalModal.ts +++ b/web/app/hooks/ExternalModal.ts @@ -7,6 +7,6 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { priority: 50, name: "external modal controller factory setup", function: async () => { - setExternalModalControllerFactory((modal, events, userData) => new ExternalModalController(modal, events, userData)); + setExternalModalControllerFactory((modal, events, userData, uniqueModalId) => new ExternalModalController(modal, events, userData, uniqueModalId)); } }); \ No newline at end of file