Implemented the channel popout ui
parent
7b120c2f57
commit
cbd20a2177
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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: [] });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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: {}
|
||||
}
|
|
@ -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>
|
||||
);
|
|
@ -38,3 +38,11 @@ html:root {
|
|||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 300px) {
|
||||
.controlBar.mode-channel-popout {
|
||||
.hideSmallPopout {
|
||||
display: none!important;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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")
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
|
@ -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();
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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));
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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(",")
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue