Implemented the channel popout ui

canary
WolverinDEV 2020-09-28 09:37:48 +02:00
parent 7b120c2f57
commit cbd20a2177
36 changed files with 1113 additions and 934 deletions

View File

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

View File

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

View File

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

View File

@ -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<Events, T = keyof Events> {
readonly type: T;
@ -217,9 +218,10 @@ export class Registry<Events extends { [key: string]: any } = { [key: string]: a
this.pendingCallbacks = undefined;
unstable_batchedUpdates(() => {
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 extends { [key: string]: any } = { [key: string]: a
}
}
export type RegistryMap = {[key: string]: any /* can't use Registry here since the template parameter is missing */ };
export function EventHandler<EventTypes>(events: (keyof EventTypes) | (keyof EventTypes)[]) {
return function (target: any,
propertyKey: string,

View File

@ -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 = (
<cbar.ControlBar ref={cbar.react_reference()} multiSession={true} />
);
ReactDOM.render(bar, $(".container-control-bar")[0]);
const events = new Registry<ControlBarEvents>()
initializeControlBarController(events, "main");
ReactDOM.render(<ControlBar2 events={events} />, $(".container-control-bar")[0]);
}
/*

View File

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

View File

@ -168,17 +168,21 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
ReactDOM.render(<ContextMenuRenderer ref={refRenderer} />, 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: [] });
}
}

View File

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

View File

@ -73,6 +73,9 @@ html:root {
flex-direction: row;
justify-content: center;
flex-grow: 0;
flex-shrink: 0;
height: 2em;
width: 2em;
@ -273,3 +276,33 @@ html:root {
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);
}
}

View File

@ -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<ButtonProperties, ButtonState> {
cssStyle.button,
switched ? cssStyle.activated : "",
typeof this.props.colorTheme === "string" ? cssStyle["theme-" + this.props.colorTheme] : "");
const button = (
<div className={buttonRootClass} title={this.props.tooltip} onClick={this.onClick.bind(this)}>
if(!this.hasChildren()) {
return (
<div className={buttonRootClass + " " + this.props.className} title={this.props.tooltip} onClick={this.onClick.bind(this)}>
<div className={this.classList("icon_em ", (switched ? this.props.iconSwitched : "") || this.props.iconNormal)} />
</div>
);
if(!this.hasChildren())
return button;
}
return (
<div className={this.classList(cssStyle.buttonDropdown, this.state.dropdownShowed || this.state.dropdownForceShow ? cssStyle.dropdownDisplayed : "", this.props.dropdownButtonExtraClass)} onMouseLeave={this.onMouseLeave.bind(this)}>
<div className={this.classList(cssStyle.buttonDropdown, this.state.dropdownShowed || this.state.dropdownForceShow ? cssStyle.dropdownDisplayed : "", this.props.className)} onMouseLeave={this.onMouseLeave.bind(this)}>
<div className={cssStyle.buttons}>
{button}
<div className={buttonRootClass} title={this.props.tooltip} onClick={this.onClick.bind(this)}>
<div className={this.classList("icon_em ", (switched ? this.props.iconSwitched : "") || this.props.iconNormal)} />
</div>
<div className={cssStyle.dropdownArrow} onMouseEnter={this.onMouseEnter.bind(this)}>
<div className={this.classList("arrow", "down")} />
<div className={cssStyle.arrow + " " + cssStyle.down} />
</div>
</div>
<DropdownContainer>

View File

@ -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<ControlBarEvents>;
private currentHandler: ConnectionHandler;
private globalEvents: (() => void)[] = [];
private globalHandlerRegisteredEvents: {[key: string]: (() => void)[]} = {};
private handlerRegisteredEvents: (() => void)[] = [];
constructor(events: Registry<ControlBarEvents>, 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<ControlBarEvents>, handler: ConnectionHandler) {
const infoHandler = initializeControlBarController(events, "channel-popout");
infoHandler.setConnectionHandler(handler);
}
export function initializeClientControlBarController(events: Registry<ControlBarEvents>) {
initializeControlBarController(events, "main");
}
export function initializeControlBarController(events: Registry<ControlBarEvents>, 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;
}

View File

@ -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: {}
}

View File

@ -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<DropdownEntry>[]
}
const LocalIconRenderer = (props: { icon?: string | RemoteIconInfo }) => {
if(!props.icon || typeof props.icon === "string") {
return <IconRenderer icon={props.icon as any} key={"fixed-icon"} />
} else {
return <RemoteIconRenderer icon={getIconManager().resolveIcon(props.icon.iconId, props.icon.serverUniqueId)} key={"remote-icon"} />;
}
}
export class DropdownEntry extends ReactComponentBase<DropdownEntryProperties, {}> {
protected defaultState() { return {}; }
render() {
if(this.props.children) {
return (
<div className={cssStyle.dropdownEntry} onClick={this.props.onClick} onAuxClick={this.props.onAuxClick} onContextMenu={this.props.onContextMenu}>
<LocalIconRenderer icon={this.props.icon} />
<a className={cssStyle.entryName}>{this.props.text}</a>
<div className={cssStyle.arrow + " " + cssStyle.right} />
<DropdownContainer>
{this.props.children}
</DropdownContainer>
</div>
);
} else {
return (
<div className={cssStyle.dropdownEntry} onClick={this.props.onClick} onAuxClick={this.props.onAuxClick} onContextMenu={this.props.onContextMenu}>
<LocalIconRenderer icon={this.props.icon} />
<a className={cssStyle.entryName}>{this.props.text}</a>
</div>
);
}
}
}
export const DropdownContainer = (props: { children: any }) => (
<div className={cssStyle.dropdown}>
{props.children}
</div>
);

View File

@ -38,3 +38,11 @@ html:root {
min-width: 0;
}
}
@media all and (max-width: 300px) {
.controlBar.mode-channel-popout {
.hideSmallPopout {
display: none!important;
}
}
}

View File

@ -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<Registry<ControlBarEvents>>(undefined);
const ModeContext = React.createContext<ControlBarMode>(undefined);
const ConnectButton = () => {
const events = useContext(Events);
const [ state, setState ] = useState<ConnectionState>(() => {
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(
<DropdownEntry key={"connect-server"} icon={"client-connect"} text={<Translatable>Connect to a server</Translatable>}
onClick={() => events.fire("action_connection_connect", { newTab: false })} />
);
} else {
subentries.push(
<DropdownEntry key={"disconnect-current-a"} icon={"client-disconnect"} text={<Translatable>Disconnect from current server</Translatable>}
onClick={() => events.fire("action_connection_disconnect", { generally: false })} />
);
}
if(state.generallyConnected) {
subentries.push(
<DropdownEntry key={"disconnect-current-b"} icon={"client-disconnect"} text={<Translatable>Disconnect from all servers</Translatable>}
onClick={() => events.fire("action_connection_disconnect", { generally: true })}/>
);
}
subentries.push(
<DropdownEntry key={"connect-new-tab"} icon={"client-connect"} text={<Translatable>Connect to a server in another tab</Translatable>}
onClick={() => events.fire("action_connection_connect", { newTab: true })} />
);
}
if(state?.currentlyConnected) {
return (
<Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-disconnect"} tooltip={tr("Disconnect from server")}
onToggle={() => events.fire("action_connection_disconnect", { generally: false })} key={"connected"}>
{subentries}
</Button>
);
} else {
return (
<Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-connect"} tooltip={tr("Connect to a server")}
onToggle={() => events.fire("action_connection_connect", { newTab: false })} key={"disconnected"}>
{subentries}
</Button>
);
}
};
const BookmarkRenderer = (props: { bookmark: Bookmark, refButton: React.RefObject<Button> }) => {
const events = useContext(Events);
if(typeof props.bookmark.children !== "undefined") {
return (
<DropdownEntry key={props.bookmark.uniqueId} text={props.bookmark.label} >
{props.bookmark.children.map(entry => <BookmarkRenderer bookmark={entry} key={entry.uniqueId} refButton={props.refButton} />)}
</DropdownEntry>
);
} else {
return (
<DropdownEntry key={props.bookmark.uniqueId}
icon={props.bookmark.icon}
text={props.bookmark.label}
onClick={() => events.fire("action_bookmark_connect", { bookmarkUniqueId: props.bookmark.uniqueId, newTab: false })}
onAuxClick={event => event.button === 1 && events.fire("action_bookmark_connect", { bookmarkUniqueId: props.bookmark.uniqueId, newTab: true })}
onContextMenu={event => {
event.preventDefault();
props.refButton.current?.setState({ dropdownForceShow: true });
spawnContextMenu({ pageY: event.pageY, pageX: event.pageX }, [
{
type: "normal",
icon: ClientIcon.Connect,
label: tr("Connect"),
click: () => events.fire("action_bookmark_connect", { bookmarkUniqueId: props.bookmark.uniqueId, newTab: false })
},
{
type: "normal",
icon: ClientIcon.Connect,
label: tr("Connect in a new tab"),
click: () => events.fire("action_bookmark_connect", { bookmarkUniqueId: props.bookmark.uniqueId, newTab: true })
}
], () => props.refButton.current?.setState({ dropdownForceShow: false }));
}}
/>
);
}
};
const BookmarkButton = () => {
const events = useContext(Events);
const mode = useContext(ModeContext);
const refButton = useRef<Button>();
const [ bookmarks, setBookmarks ] = useState<Bookmark[]>(() => {
events.fire("query_bookmarks");
return [];
});
events.reactUse("notify_bookmarks", event => setBookmarks(event.marks.slice()));
let entries = [];
if(mode === "main") {
entries.push(
<DropdownEntry icon={"client-bookmark_manager"} text={<Translatable>Manage bookmarks</Translatable>}
onClick={() => events.fire("action_bookmark_manage")} key={"manage"} />
);
entries.push(
<DropdownEntry icon={"client-bookmark_add"} text={<Translatable>Add current server to bookmarks</Translatable>}
onClick={() => events.fire("action_bookmark_add_current_server")} key={"add"} />
);
}
if(bookmarks.length > 0) {
if(entries.length > 0) {
entries.push(<hr key={"hr"} />);
}
entries.push(...bookmarks.map(mark => <BookmarkRenderer key={mark.uniqueId} bookmark={mark} refButton={refButton} />));
}
return (
<Button ref={refButton} className={cssButtonStyle.buttonBookmarks + " " + cssStyle.hideSmallPopout} autoSwitch={false} iconNormal={"client-bookmark_manager"}>
{entries}
</Button>
)
};
const AwayButton = () => {
const events = useContext(Events);
const [ state, setState ] = useState<AwayState>(() => {
events.fire("query_away_state");
return undefined;
});
events.on("notify_away_state", event => setState(event.state));
let dropdowns = [];
if(state?.locallyAway) {
dropdowns.push(<DropdownEntry key={"cgo"} icon={ClientIcon.Present} text={<Translatable>Go online</Translatable>}
onClick={() => events.fire("action_toggle_away", { away: false, globally: false })} />);
} else {
dropdowns.push(<DropdownEntry key={"sas"} icon={ClientIcon.Away} text={<Translatable>Set away on this server</Translatable>}
onClick={() => events.fire("action_toggle_away", { away: true, globally: false })} />);
}
dropdowns.push(<DropdownEntry key={"sam"} icon={ClientIcon.Away} text={<Translatable>Set away message on this server</Translatable>}
onClick={() => events.fire("action_toggle_away", { away: true, globally: false, promptMessage: true })} />);
dropdowns.push(<hr key={"-hr"} />);
if(state?.globallyAway !== "none") {
dropdowns.push(<DropdownEntry key={"goa"} icon={ClientIcon.Present} text={<Translatable>Go online for all servers</Translatable>}
onClick={() => events.fire("action_toggle_away", { away: false, globally: true })} />);
}
if(state?.globallyAway !== "full") {
dropdowns.push(<DropdownEntry key={"saa"} icon={ClientIcon.Away} text={<Translatable>Set away on all servers</Translatable>}
onClick={() => events.fire("action_toggle_away", { away: true, globally: true })} />);
}
dropdowns.push(<DropdownEntry key={"sama"} icon={ClientIcon.Away} text={<Translatable>Set away message for all servers</Translatable>}
onClick={() => events.fire("action_toggle_away", { away: true, globally: true, promptMessage: true })} />);
return (
<Button
autoSwitch={false}
switched={!!state?.locallyAway}
iconNormal={ClientIcon.Away}
iconSwitched={ClientIcon.Present}
onToggle={target => events.fire("action_toggle_away", { away: target, globally: false })}
>
{dropdowns}
</Button>
);
};
const MicrophoneButton = () => {
const events = useContext(Events);
const [ state, setState ] = useState<MicrophoneState>(() => {
events.fire("query_microphone_state");
return undefined;
});
events.on("notify_microphone_state", event => setState(event.state));
if(state === "muted") {
return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={"client-input_muted"} tooltip={tr("Unmute microphone")}
onToggle={() => events.fire("action_toggle_microphone", { enabled: true })} key={"muted"} />;
} else if(state === "enabled") {
return <Button colorTheme={"red"} autoSwitch={false} iconNormal={"client-input_muted"} tooltip={tr("Mute microphone")}
onToggle={() => events.fire("action_toggle_microphone", { enabled: false })} key={"enabled"} />;
} else {
return <Button autoSwitch={false} iconNormal={"client-activate_microphone"} tooltip={tr("Enable your microphone on this server")}
onToggle={() => events.fire("action_toggle_microphone", { enabled: true })} key={"disabled"} />;
}
}
const SpeakerButton = () => {
const events = useContext(Events);
const [ enabled, setEnabled ] = useState<boolean>(() => {
events.fire("query_speaker_state");
return true;
});
events.on("notify_speaker_state", event => setEnabled(event.enabled));
if(enabled) {
return <Button colorTheme={"red"} autoSwitch={false} iconNormal={ClientIcon.OutputMuted} tooltip={tr("Mute headphones")}
onToggle={() => events.fire("action_toggle_speaker", { enabled: false })} key={"enabled"} />;
} else {
return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={ClientIcon.OutputMuted} tooltip={tr("Unmute headphones")}
onToggle={() => events.fire("action_toggle_speaker", { enabled: true })} key={"disabled"} />;
}
}
const SubscribeButton = () => {
const events = useContext(Events);
const [ subscribe, setSubscribe ] = useState<boolean>(() => {
events.fire("query_subscribe_state");
return true;
});
events.on("notify_subscribe_state", event => setSubscribe(event.subscribe));
return <Button switched={subscribe}
autoSwitch={false}
iconNormal={ClientIcon.UnsubscribeFromAllChannels}
iconSwitched={ClientIcon.SubscribeToAllChannels}
className={cssStyle.hideSmallPopout}
onToggle={flag => events.fire("action_toggle_subscribe", { subscribe: flag })}
/>;
}
const QueryButton = () => {
const events = useContext(Events);
const mode = useContext(ModeContext);
const [ shown, setShown ] = useState<boolean>(() => {
events.fire("query_query_state");
return true;
});
events.on("notify_query_state", event => setShown(event.shown));
if(mode === "channel-popout") {
return (
<Button switched={shown}
autoSwitch={false}
iconNormal={ClientIcon.ServerQuery}
className={cssStyle.hideSmallPopout}
onToggle={flag => events.fire("action_toggle_query", { show: flag })}
key={"mode-channel-popout"}
/>
);
} else {
let toggle;
if(shown) {
toggle = <DropdownEntry key={"query-show"} icon={ClientIcon.ToggleServerQueryClients} text={<Translatable>Hide server queries</Translatable>}
onClick={() => events.fire("action_toggle_query", { show: false })} />;
} else {
toggle = <DropdownEntry key={"query-hide"} icon={ClientIcon.ToggleServerQueryClients} text={<Translatable>Show server queries</Translatable>}
onClick={() => events.fire("action_toggle_query", { show: true })}/>;
}
return (
<Button switched={shown}
autoSwitch={false}
iconNormal={ClientIcon.ServerQuery}
className={cssStyle.hideSmallPopout}
onToggle={flag => events.fire("action_toggle_query", { show: flag })}
key={"mode-full"}
>
{toggle}
<DropdownEntry icon={ClientIcon.ServerQuery} text={<Translatable>Manage server queries</Translatable>}
onClick={() => events.fire("action_query_manage")}/>
</Button>
);
}
};
const HostButton = () => {
const events = useContext(Events);
const [ hostButton, setHostButton ] = useState<HostButtonInfo>(() => {
events.fire("query_host_button");
return undefined;
});
events.reactUse("notify_host_button", event => setHostButton(event.button));
if(!hostButton) {
return null;
} else {
return (
<a
className={cssButtonStyle.button + " " + cssButtonStyle.buttonHostbutton + " " + cssStyle.hideSmallPopout}
title={hostButton.title || tr("Hostbutton")}
onClick={event => {
window.open(hostButton.target || hostButton.url, '_blank');
event.preventDefault();
}}
>
<img alt={tr("Hostbutton")} src={hostButton.url} />
</a>
);
}
};
export const ControlBar2 = (props: { events: Registry<ControlBarEvents>, className?: string }) => {
const [ mode, setMode ] = useState<ControlBarMode>(() => {
props.events.fire("query_mode");
return undefined;
});
props.events.reactUse("notify_mode", event => setMode(event.mode));
const items = [];
if(mode !== "channel-popout") {
items.push(<ConnectButton key={"connect"} />);
}
items.push(<BookmarkButton key={"bookmarks"} />);
items.push(<div className={cssStyle.divider + " " + cssStyle.hideSmallPopout} />);
items.push(<AwayButton key={"away"} />);
items.push(<MicrophoneButton key={"microphone"} />);
items.push(<SpeakerButton key={"speaker"} />);
items.push(<div className={cssStyle.divider + " " + cssStyle.hideSmallPopout} />);
items.push(<SubscribeButton key={"subscribe"} />);
items.push(<QueryButton key={"query"} />);
items.push(<div className={cssStyle.spacer} />);
items.push(<HostButton key={"hostbutton"} />);
return (
<Events.Provider value={props.events}>
<ModeContext.Provider value={mode}>
<div className={cssStyle.controlBar + " " + cssStyle["mode-" + mode]}>
{items}
</div>
</ModeContext.Provider>
</Events.Provider>
)
};

View File

@ -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<DropdownEntryProperties, {}> {
protected defaultState() { return {}; }
render() {
if(this.props.children) {
return (
<div className={cssStyle.dropdownEntry} onClick={this.props.onClick} onContextMenu={this.props.onContextMenu}>
{typeof this.props.icon === "string" ? <IconRenderer icon={this.props.icon} /> :
<RemoteIconRenderer icon={getIconManager().resolveIcon(this.props.icon.iconId, this.props.icon.serverUniqueId)} />
}
<a className={cssStyle.entryName}>{this.props.text}</a>
<div className={this.classList("arrow", "right")} />
<DropdownContainer>
{this.props.children}
</DropdownContainer>
</div>
);
} else {
return (
<div className={cssStyle.dropdownEntry} onClick={this.props.onClick} onContextMenu={this.props.onContextMenu}>
{typeof this.props.icon === "string" ? <IconRenderer icon={this.props.icon} /> :
<RemoteIconRenderer icon={getIconManager().resolveIcon(this.props.icon.iconId, this.props.icon.serverUniqueId)} />
}
<a className={cssStyle.entryName}>{this.props.text}</a>
</div>
);
}
}
}
export interface DropdownContainerProperties { }
export interface DropdownContainerState { }
export class DropdownContainer extends ReactComponentBase<DropdownContainerProperties, DropdownContainerState> {
protected defaultState() {
return { };
}
render() {
return (
<div className={this.classList(cssStyle.dropdown)}>
{this.props.children}
</div>
);
}
}

View File

@ -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<InternalControlBarEvents> }, ConnectionState> {
protected defaultState(): ConnectionState {
return {
connected: false,
connectedAnywhere: false
}
}
render() {
let subentries = [];
if(this.props.multiSession) {
if(!this.state.connected) {
subentries.push(
<DropdownEntry key={"connect-server"} icon={"client-connect"} text={<Translatable>Connect to a server</Translatable>}
onClick={ () => global_client_actions.fire("action_open_window_connect", {newTab: false }) } />
);
} else {
subentries.push(
<DropdownEntry key={"disconnect-current-a"} icon={"client-disconnect"} text={<Translatable>Disconnect from current server</Translatable>}
onClick={ () => this.props.event_registry.fire("action_disconnect", { globally: false }) }/>
);
}
if(this.state.connectedAnywhere) {
subentries.push(
<DropdownEntry key={"disconnect-current-b"} icon={"client-disconnect"} text={<Translatable>Disconnect from all servers</Translatable>}
onClick={ () => this.props.event_registry.fire("action_disconnect", { globally: true }) }/>
);
}
subentries.push(
<DropdownEntry key={"connect-new-tab"} icon={"client-connect"} text={<Translatable>Connect to a server in another tab</Translatable>}
onClick={ () => global_client_actions.fire("action_open_window_connect", { newTab: true }) } />
);
}
if(!this.state.connected) {
return (
<Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-connect"} tooltip={tr("Connect to a server")}
onToggle={ () => global_client_actions.fire("action_open_window_connect", { newTab: false }) }>
{subentries}
</Button>
);
} else {
return (
<Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-disconnect"} tooltip={tr("Disconnect from server")}
onToggle={ () => this.props.event_registry.fire("action_disconnect", { globally: false }) }>
{subentries}
</Button>
);
}
}
@EventHandler<InternalControlBarEvents>("update_connect_state")
private handleStateUpdate(state: ConnectionState) {
this.setState(state);
}
}
@ReactEventHandler(obj => obj.props.event_registry)
class BookmarkButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, {}> {
private button_ref: React.RefObject<Button>;
protected initialize() {
this.button_ref = React.createRef();
}
protected defaultState() {
return {};
}
render() {
const marks = bookmarks().content.map(e => e.type === BookmarkType.DIRECTORY ? this.renderDirectory(e) : this.renderBookmark(e));
if(marks.length)
marks.splice(0, 0, <hr key={"hr"} />);
return (
<Button ref={this.button_ref} dropdownButtonExtraClass={cssButtonStyle.buttonBookmarks} autoSwitch={false} iconNormal={"client-bookmark_manager"}>
<DropdownEntry icon={"client-bookmark_manager"} text={<Translatable>Manage bookmarks</Translatable>}
onClick={() => this.props.event_registry.fire("action_open_window", { window: "bookmark-manage" })} />
<DropdownEntry icon={"client-bookmark_add"} text={<Translatable>Add current server to bookmarks</Translatable>}
onClick={() => this.props.event_registry.fire("action_add_current_server_to_bookmarks")} />
{marks}
</Button>
)
}
private renderBookmark(bookmark: Bookmark) {
return (
<DropdownEntry key={bookmark.unique_id}
icon={{ iconId: bookmark.last_icon_id, serverUniqueId: bookmark.last_icon_server_id }}
text={bookmark.display_name}
onClick={BookmarkButton.onBookmarkClick.bind(undefined, bookmark.unique_id)}
onContextMenu={this.onBookmarkContextMenu.bind(this, bookmark.unique_id)}/>
);
}
private renderDirectory(directory: DirectoryBookmark) {
return (
<DropdownEntry key={directory.unique_id} text={directory.display_name} >
{directory.content.map(e => e.type === BookmarkType.DIRECTORY ? this.renderDirectory(e) : this.renderBookmark(e))}
</DropdownEntry>
)
}
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<InternalControlBarEvents>("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<InternalControlBarEvents> }, AwayState> {
protected defaultState(): AwayState {
return {
away: false,
awayAnywhere: false,
awayAll: false
};
}
render() {
let dropdowns = [];
if(this.state.away) {
dropdowns.push(<DropdownEntry key={"cgo"} icon={"client-present"} text={<Translatable>Go online</Translatable>}
onClick={() => this.props.event_registry.fire("action_disable_away", { globally: false })} />);
} else {
dropdowns.push(<DropdownEntry key={"sas"} icon={"client-away"} text={<Translatable>Set away on this server</Translatable>}
onClick={() => this.props.event_registry.fire("action_set_away", { globally: false, prompt_reason: false })} />);
}
dropdowns.push(<DropdownEntry key={"sam"} icon={"client-away"} text={<Translatable>Set away message on this server</Translatable>}
onClick={() => this.props.event_registry.fire("action_set_away", { globally: false, prompt_reason: true })} />);
dropdowns.push(<hr key={"-hr"} />);
if(this.state.awayAnywhere) {
dropdowns.push(<DropdownEntry key={"goa"} icon={"client-present"} text={<Translatable>Go online for all servers</Translatable>}
onClick={() => this.props.event_registry.fire("action_disable_away", { globally: true })} />);
}
if(!this.state.awayAll) {
dropdowns.push(<DropdownEntry key={"saa"} icon={"client-away"} text={<Translatable>Set away on all servers</Translatable>}
onClick={() => this.props.event_registry.fire("action_set_away", { globally: true, prompt_reason: false })} />);
}
dropdowns.push(<DropdownEntry key={"sama"} icon={"client-away"} text={<Translatable>Set away message for all servers</Translatable>}
onClick={() => this.props.event_registry.fire("action_set_away", { globally: true, prompt_reason: true })} />);
/* switchable because we're switching it manually */
return (
<Button autoSwitch={false} switched={this.state.away} iconNormal={this.state.away ? "client-present" : "client-away"} onToggle={this.handleButtonToggled.bind(this)}>
{dropdowns}
</Button>
);
}
private handleButtonToggled(state: boolean) {
if(state)
this.props.event_registry.fire("action_set_away", { globally: false, prompt_reason: false });
else
this.props.event_registry.fire("action_disable_away");
}
@EventHandler<InternalControlBarEvents>("update_away_state")
private handleStateUpdate(state: AwayState) {
this.setState(state);
}
}
export interface ChannelSubscribeState {
subscribeEnabled: boolean;
}
@ReactEventHandler(obj => obj.props.event_registry)
class ChannelSubscribeButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, ChannelSubscribeState> {
protected defaultState(): ChannelSubscribeState {
return { subscribeEnabled: false };
}
render() {
return <Button switched={this.state.subscribeEnabled} autoSwitch={false} iconNormal={"client-unsubscribe_from_all_channels"} iconSwitched={"client-subscribe_to_all_channels"}
onToggle={flag => this.props.event_registry.fire("action_set_subscribe", { subscribe: flag })}/>;
}
@EventHandler<InternalControlBarEvents>("update_subscribe_state")
private handleStateUpdate(state: ChannelSubscribeState) {
this.setState(state);
}
}
export interface MicrophoneState {
enabled: boolean;
muted: boolean;
}
@ReactEventHandler(obj => obj.props.event_registry)
class MicrophoneButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, MicrophoneState> {
protected defaultState(): MicrophoneState {
return {
enabled: false,
muted: false
};
}
render() {
if(!this.state.enabled)
return <Button autoSwitch={false} iconNormal={"client-activate_microphone"} tooltip={tr("Enable your microphone on this server")}
onToggle={() => this.props.event_registry.fire("action_enable_microphone")} />;
if(this.state.muted)
return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={"client-input_muted"} tooltip={tr("Unmute microphone")}
onToggle={() => this.props.event_registry.fire("action_enable_microphone")} />;
return <Button colorTheme={"red"} autoSwitch={false} iconNormal={"client-input_muted"} tooltip={tr("Mute microphone")}
onToggle={() => this.props.event_registry.fire("action_disable_microphone")} />;
}
@EventHandler<InternalControlBarEvents>("update_microphone_state")
private handleStateUpdate(state: MicrophoneState) {
this.setState(state);
}
}
export interface SpeakerState {
muted: boolean;
}
@ReactEventHandler(obj => obj.props.event_registry)
class SpeakerButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, SpeakerState> {
protected defaultState(): SpeakerState {
return {
muted: false
};
}
render() {
if(this.state.muted)
return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={"client-output_muted"} tooltip={tr("Unmute headphones")}
onToggle={() => this.props.event_registry.fire("action_enable_speaker")}/>;
return <Button colorTheme={"red"} autoSwitch={false} iconNormal={"client-output_muted"} tooltip={tr("Mute headphones")}
onToggle={() => this.props.event_registry.fire("action_disable_speaker")}/>;
}
@EventHandler<InternalControlBarEvents>("update_speaker_state")
private handleStateUpdate(state: SpeakerState) {
this.setState(state);
}
}
export interface QueryState {
queryShown: boolean;
}
@ReactEventHandler(obj => obj.props.event_registry)
class QueryButton extends ReactComponentBase<{ event_registry: Registry<InternalControlBarEvents> }, QueryState> {
protected defaultState() {
return {
queryShown: false
};
}
render() {
let toggle;
if(this.state.queryShown)
toggle = <DropdownEntry key={"query-show"} icon={"client-toggle_server_query_clients"} text={<Translatable>Hide server queries</Translatable>}
onClick={() => this.props.event_registry.fire("action_toggle_query", { shown: false })}/>;
else
toggle = <DropdownEntry key={"query-hide"} icon={"client-toggle_server_query_clients"} text={<Translatable>Show server queries</Translatable>}
onClick={() => this.props.event_registry.fire("action_toggle_query", { shown: true })}/>;
return (
<Button switched={this.state.queryShown} autoSwitch={false} iconNormal={"client-server_query"}
onToggle={flag => this.props.event_registry.fire("action_toggle_query", { shown: flag })}>
{toggle}
<DropdownEntry icon={"client-server_query"} text={<Translatable>Manage server queries</Translatable>}
onClick={() => this.props.event_registry.fire("action_open_window", { window: "query-manage" })}/>
</Button>
)
}
@EventHandler<InternalControlBarEvents>("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<InternalControlBarEvents> }, HostButtonState> {
protected defaultState() {
return {
url: undefined,
target_url: undefined
};
}
render() {
if(!this.state.url)
return null;
return (
<a
className={this.classList(cssButtonStyle.button, cssButtonStyle.buttonHostbutton)}
title={this.state.title || tr("Hostbutton")}
href={this.state.target_url || this.state.url}
target={"_blank"} /* just to ensure */
onClick={this.onClick.bind(this)}>
<img alt={tr("Hostbutton")} src={this.state.url} />
</a>
);
}
private onClick(event: MouseEvent) {
window.open(this.state.target_url || this.state.url, '_blank');
event.preventDefault();
}
@EventHandler<InternalControlBarEvents>("update_host_button")
private handleStateUpdate(state: HostButtonState) {
this.setState(state);
}
}
export interface ControlBarProperties {
multiSession: boolean;
}
@ReactEventHandler<ControlBar>(obj => obj.event_registry)
export class ControlBar extends React.Component<ControlBarProperties, {}> {
private readonly event_registry: Registry<InternalControlBarEvents>;
private connection: ConnectionHandler;
private connection_handler_callbacks = {
notify_state_updated: this.handleConnectionHandlerStateChange.bind(this),
notify_connection_state_changed: this.handleConnectionHandlerConnectionStateChange.bind(this)
};
private connection_manager_callbacks = {
active_handler_changed: this.handleActiveConnectionHandlerChanged.bind(this)
};
constructor(props) {
super(props);
this.event_registry = new Registry<InternalControlBarEvents>();
this.event_registry.enableDebug("control-bar");
initialize(this.event_registry);
}
events() : Registry<InternalControlBarEvents> { return this.event_registry; }
render() {
return (
<div className={cssStyle.controlBar}>
<ConnectButton event_registry={this.event_registry} multiSession={this.props.multiSession} />
<BookmarkButton event_registry={this.event_registry} />
<div className={cssStyle.divider} />
<AwayButton event_registry={this.event_registry} />
<MicrophoneButton event_registry={this.event_registry} />
<SpeakerButton event_registry={this.event_registry} />
<div className={cssStyle.divider} />
<ChannelSubscribeButton event_registry={this.event_registry} />
<QueryButton event_registry={this.event_registry} />
<div className={cssStyle.spacer} />
<HostButton event_registry={this.event_registry} />
</div>
)
}
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<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateHostButton(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "host-button" && event.as<"update_state">().state !== "connect-state")
return;
const 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<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateSubscribe(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "subscribe-mode")
return;
this.event_registry.fire("update_subscribe_state", {
subscribeEnabled: !!this.connection?.isSubscribeToAllChannels()
});
}
@EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateConnect(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "connect-state")
return;
this.event_registry.fire("update_connect_state", {
connectedAnywhere: server_connections.all_connections().findIndex(e => e.connected) !== -1,
connected: !!this.connection?.connected
});
}
@EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateAway(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "away")
return;
const connections = server_connections.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<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateMicrophone(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "microphone")
return;
this.event_registry.fire("update_microphone_state", {
enabled: !this.connection?.isMicrophoneDisabled(),
muted: !!this.connection?.isMicrophoneMuted()
});
}
@EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateSpeaker(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "speaker")
return;
this.event_registry.fire("update_speaker_state", {
muted: !!this.connection?.isSpeakerMuted()
});
}
@EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateQuery(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "query")
return;
this.event_registry.fire("update_query_state", {
queryShown: !!this.connection?.areQueriesShown()
});
}
@EventHandler<InternalControlBarEvents>(["update_state_all", "update_state"])
private updateStateBookmarks(event: Event<InternalControlBarEvents>) {
if(event.type === "update_state")
if(event.as<"update_state">().state !== "bookmarks")
return;
this.event_registry.fire("update_bookmarks");
}
}
let react_reference_: React.RefObject<ControlBar>;
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<InternalControlBarEvents>) {
let current_connection_handler: ConnectionHandler;
event_registry.on("set_connection_handler", event => current_connection_handler = event.handler);
event_registry.on("action_disconnect", event => {
(event.globally ? server_connections.all_connections() : [server_connections.active_connection()]).filter(e => !!e).forEach(connection => {
connection.disconnectFromServer();
});
});
event_registry.on("action_set_away", event => {
const set_away = message => {
const value = typeof message === "string" ? message : true;
(event.globally ? server_connections.all_connections() : [server_connections.active_connection()]).filter(e => !!e).forEach(connection => {
connection.setAway(value);
});
settings.changeGlobal(Settings.KEY_CLIENT_STATE_AWAY, true);
settings.changeGlobal(Settings.KEY_CLIENT_AWAY_MESSAGE, typeof value === "boolean" ? "" : value);
};
if(event.prompt_reason) {
createInputModal(tr("Set away message"), tr("Please enter your away message"), () => true, message => {
if(typeof(message) === "string")
set_away(message);
}).open();
} else {
set_away(undefined);
}
});
event_registry.on("action_disable_away", event => {
for(const connection of event.globally ? server_connections.all_connections() : [server_connections.active_connection()]) {
if(!connection) continue;
connection.setAway(false);
}
settings.changeGlobal(Settings.KEY_CLIENT_STATE_AWAY, false);
});
event_registry.on(["action_enable_microphone", "action_disable_microphone"], event => {
const state = event.type === "action_enable_microphone";
/* change the default global setting */
settings.changeGlobal(Settings.KEY_CLIENT_STATE_MICROPHONE_MUTED, !state);
if(current_connection_handler) {
current_connection_handler.setMicrophoneMuted(!state);
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;
}
})
}

View File

@ -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<ConversationUIEvents>;
private readonly userData: any;
constructor(events: Registry<ConversationUIEvents>, userData: any) {
constructor(registryMap: RegistryMap, userData: any) {
super();
this.userData = userData;
this.events = events;
this.events = registryMap["default"] as any;
}
renderBody() {

View File

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

View File

@ -171,7 +171,7 @@ export function spawnModalCssVariableEditor() {
const events = new Registry<CssEditorEvents>();
cssVariableEditorController(events);
const modal = spawnExternalModal("css-editor", events, {});
const modal = spawnExternalModal("css-editor", { default: events }, {});
modal.show();
}

View File

@ -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<CssEditorEvents>;
private readonly userData: CssEditorUserData;
constructor(events: Registry<CssEditorEvents>, 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();

View File

@ -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<ModalEvents>();
@ -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;
}

View File

@ -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<Type extends "controller" | "popout">
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<T extends keyof {}>(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<T extends keyof {}>(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<Type extends "controller" | "popout">
}
}
};
this.localEventRegistry.connectAll(this.localEventReceiver);
}
protected handleIPCMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
@ -97,7 +109,7 @@ export abstract class EventControllerBase<Type extends "controller" | "popout">
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<Type extends "controller" | "popout">
}
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 = {};

View File

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

View File

@ -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 <T>(events: Registry<T>, userData: any) => AbstractModal;
let modalClass: new (events: RegistryMap, userData: any) => AbstractModal;
const kSettingModalTarget: SettingsKey<string> = {
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");

View File

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

View File

@ -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<EventClass extends { [key: string]: any }>(modal: string, events: Registry<EventClass>, userData: any) : ModalController {
export function spawnExternalModal<EventClass extends { [key: string]: any }>(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);
}

View File

@ -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<ChannelTreeUIEvents>();
events.enableDebug("channel-tree-view");
initializeTreeController(events, channelTree);
initializeChannelTreeController(events, channelTree);
ReactDOM.render([
ReactDOM.render(
<ChannelTreeRenderer handlerId={channelTree.client.handlerId} events={events} />
//<TreeEntryMove key={"move"} onMoveEnd={(point) => this.onMoveEnd(point.x, point.y)} ref={this.view_move} />
], target);
, target);
/*
(window as any).chan_pop = () => {
const events = new Registry<ChannelTreeUIEvents>();
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<ChannelTreeUIEvents>, channelTree: ChannelTree) {
export function initializeChannelTreeController(events: Registry<ChannelTreeUIEvents>, channelTree: ChannelTree) {
/* initialize the general update handler */
const controller = new ChannelTreeController(events, channelTree);
controller.initialize();

View File

@ -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<ChannelTreeUIEvents>;
readonly handlerId: string;
constructor(registry: Registry<ChannelTreeUIEvents>, userData: any) {
super();
this.handlerId = userData.handlerId;
this.events = registry;
}
renderBody(): React.ReactElement {
return <ChannelTreeRenderer events={this.events} handlerId={this.handlerId} />;
}
title(): string | React.ReactElement<Translatable> {
return <Translatable>Channel tree</Translatable>;
}
}
export = ChannelTreeModal;

View File

@ -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<ChannelTreeUIEvents>();
eventsTree.enableDebug("channel-tree-view-modal");
initializeChannelTreeController(eventsTree, handler.channelTree);
const eventsControlBar = new Registry<ControlBarEvents>();
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();
});
}

View File

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

View File

@ -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<ChannelTreeUIEvents>;
readonly eventsControlBar: Registry<ControlBarEvents>;
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 (
<div className={cssStyle.container}>
<div className={cssStyle.containerControlBar}>
<ControlBar2 events={this.eventsControlBar} className={cssStyle.containerControlBar} />
</div>
<div className={cssStyle.containerChannelTree}>
<ChannelTreeRenderer events={this.eventsTree} handlerId={this.handlerId} />
</div>
</div>
)
}
title(): string | React.ReactElement<Translatable> {
return <Translatable>Channel tree</Translatable>;
}
}
export = ChannelTreeModal;

View File

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

View File

@ -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<VideoViewerEvents>;
readonly handlerId: string;
constructor(registry: Registry<VideoViewerEvents>, 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<Translatable> {

View File

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

View File

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