diff --git a/loader/app/targets/maifest-target.ts b/loader/app/targets/maifest-target.ts index 4fe8d7b7..70432e5a 100644 --- a/loader/app/targets/maifest-target.ts +++ b/loader/app/targets/maifest-target.ts @@ -50,24 +50,6 @@ export default class implements ApplicationLoader { container.setAttribute('id', "sounds"); body.append(container); } - - /* mouse move container */ - { - const container = document.createElement("div"); - container.setAttribute('id', "mouse-move"); - - body.append(container); - } - - /* tooltip container */ - { - const container = document.createElement("div"); - container.setAttribute('id', "global-tooltip"); - - container.append(document.createElement("a")); - - body.append(container); - } }, priority: 10 }); diff --git a/shared/js/tree/ChannelTree.tsx b/shared/js/tree/ChannelTree.tsx index 3ef85f6e..45251b69 100644 --- a/shared/js/tree/ChannelTree.tsx +++ b/shared/js/tree/ChannelTree.tsx @@ -26,6 +26,7 @@ import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; import {tra} from "tc-shared/i18n/localize"; import {EventType} from "tc-shared/ui/frames/log/Definitions"; import {renderChannelTree} from "tc-shared/ui/tree/Controller"; +import {ChannelTreePopoutController} from "tc-shared/ui/tree/popout/Controller"; export interface ChannelTreeEvents { action_select_entries: { @@ -43,6 +44,7 @@ export interface ChannelTreeEvents { notify_tree_reset: {}, notify_selection_changed: {}, notify_query_view_state_changed: { queries_shown: boolean }, + notify_popout_state_changed: { popoutShown: boolean }, notify_entry_move_begin: {}, notify_entry_move_end: {}, @@ -231,19 +233,18 @@ export class ChannelTree { /* whatever all channels have been initiaized */ channelsInitialized: boolean = false; - //readonly view: React.RefObject; - //readonly view_move: React.RefObject; readonly selection: ChannelTreeEntrySelect; + readonly popoutController: ChannelTreePopoutController; - private readonly _tag_container: JQuery; + private readonly tagContainer: JQuery; private _show_queries: boolean; private channel_last?: ChannelEntry; private channel_first?: ChannelEntry; - private _tag_container_focused = false; - private _listener_document_click; - private _listener_document_key; + private tagContainerFocused = false; + private listenerDocumentClick; + private listenerDocumentKeyPress; constructor(client) { this.events = new Registry(); @@ -253,21 +254,16 @@ export class ChannelTree { this.server = new ServerEntry(this, "undefined", undefined); this.selection = new ChannelTreeEntrySelect(this); + this.popoutController = new ChannelTreePopoutController(this); - this._tag_container = $.spawn("div").addClass("channel-tree-container"); - renderChannelTree(this, this._tag_container[0]); - /* - ReactDOM.render([ - this.onChannelEntryMove(a, b)} tree={this} ref={this.view} />, - this.onMoveEnd(point.x, point.y)} ref={this.view_move} /> - ], this._tag_container[0]); - */ + this.tagContainer = $.spawn("div").addClass("channel-tree-container"); + renderChannelTree(this, this.tagContainer[0], { popoutButton: true }); this.reset(); if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) { /* - TODO: Move this into the channel tree renderer + TODO: Show the context menu when clicked on no channel this._tag_container.on("contextmenu", (event) => { event.preventDefault(); @@ -284,24 +280,25 @@ export class ChannelTree { */ } - this._listener_document_key = event => this.handle_key_press(event); - this._listener_document_click = event => { - this._tag_container_focused = false; + /* FIXME: Move this to the channel tree renderer */ + this.listenerDocumentKeyPress = event => this.handle_key_press(event); + this.listenerDocumentClick = event => { + this.tagContainerFocused = false; let element = event.target as HTMLElement; while(element) { - if(element === this._tag_container[0]) { - this._tag_container_focused = true; + if(element === this.tagContainer[0]) { + this.tagContainerFocused = true; break; } element = element.parentNode as HTMLElement; } }; - document.addEventListener('click', this._listener_document_click); - document.addEventListener('keydown', this._listener_document_key); + document.addEventListener('click', this.listenerDocumentClick); + document.addEventListener('keydown', this.listenerDocumentKeyPress); } tag_tree() : JQuery { - return this._tag_container; + return this.tagContainer; } channelsOrdered() : ChannelEntry[] { @@ -337,13 +334,13 @@ export class ChannelTree { } destroy() { - ReactDOM.unmountComponentAtNode(this._tag_container[0]); + ReactDOM.unmountComponentAtNode(this.tagContainer[0]); - this._listener_document_click && document.removeEventListener('click', this._listener_document_click); - this._listener_document_click = undefined; + this.listenerDocumentClick && document.removeEventListener('click', this.listenerDocumentClick); + this.listenerDocumentClick = undefined; - this._listener_document_key && document.removeEventListener('keydown', this._listener_document_key); - this._listener_document_key = undefined; + this.listenerDocumentKeyPress && document.removeEventListener('keydown', this.listenerDocumentKeyPress); + this.listenerDocumentKeyPress = undefined; if(this.server) { this.server.destroy(); @@ -354,7 +351,8 @@ export class ChannelTree { this.channel_first = undefined; this.channel_last = undefined; - this._tag_container.remove(); + this.popoutController.destroy(); + this.tagContainer.remove(); this.selection.destroy(); this.events.destroy(); } @@ -992,7 +990,7 @@ export class ChannelTree { } handle_key_press(event: KeyboardEvent) { - if(!this._tag_container_focused || !this.selection.is_anything_selected() || this.selection.is_multi_select()) return; + if(!this.tagContainerFocused || !this.selection.is_anything_selected() || this.selection.is_multi_select()) return; const selected = this.selection.selected_entries[0]; if(event.keyCode == KeyCode.KEY_UP) { diff --git a/shared/js/ui/frames/control-bar/Renderer.tsx b/shared/js/ui/frames/control-bar/Renderer.tsx index 92edee75..86927322 100644 --- a/shared/js/ui/frames/control-bar/Renderer.tsx +++ b/shared/js/ui/frames/control-bar/Renderer.tsx @@ -301,7 +301,7 @@ const QueryButton = () => { > {toggle} Manage server queries} - onClick={() => events.fire("action_query_manage")}/> + onClick={() => events.fire("action_query_manage")} key={"manage-entries"} /> ); } @@ -347,16 +347,16 @@ export const ControlBar2 = (props: { events: Registry, classNa if(mode !== "channel-popout") { items.push(); + items.push(); + items.push(
); } - items.push(); - items.push(
); items.push(); items.push(); items.push(); - items.push(
); + items.push(
); items.push(); items.push(); - items.push(
); + items.push(
); items.push(); return ( diff --git a/shared/js/ui/react-elements/Icon.scss b/shared/js/ui/react-elements/Icon.scss new file mode 100644 index 00000000..eebe38b1 --- /dev/null +++ b/shared/js/ui/react-elements/Icon.scss @@ -0,0 +1,5 @@ +.empty { + /* legacy values, we're using em now */ + width: 16px; + height: 16px; +} \ No newline at end of file diff --git a/shared/js/ui/react-elements/Icon.tsx b/shared/js/ui/react-elements/Icon.tsx index 763dcaf4..32d712ce 100644 --- a/shared/js/ui/react-elements/Icon.tsx +++ b/shared/js/ui/react-elements/Icon.tsx @@ -2,13 +2,15 @@ import * as React from "react"; import {RemoteIcon} from "tc-shared/file/Icons"; import {useState} from "react"; +const cssStyle = require("./Icon.scss"); + export const IconRenderer = (props: { icon: string; title?: string; className?: string; }) => { if(!props.icon) { - return
; + return
; } else if(typeof props.icon === "string") { return
; } else { diff --git a/shared/js/ui/react-elements/ModalDefinitions.ts b/shared/js/ui/react-elements/ModalDefinitions.ts index cc38561f..fdb4be42 100644 --- a/shared/js/ui/react-elements/ModalDefinitions.ts +++ b/shared/js/ui/react-elements/ModalDefinitions.ts @@ -40,7 +40,7 @@ export abstract class AbstractModal { protected constructor() {} abstract renderBody() : ReactElement; - abstract title() : string | React.ReactElement; + abstract title() : string | React.ReactElement; /* only valid for the "inline" modals */ type() : ModalType { return "none"; } diff --git a/shared/js/ui/react-elements/external-modal/Controller.ts b/shared/js/ui/react-elements/external-modal/Controller.ts index 0dc4af25..06d7d49e 100644 --- a/shared/js/ui/react-elements/external-modal/Controller.ts +++ b/shared/js/ui/react-elements/external-modal/Controller.ts @@ -108,8 +108,9 @@ export abstract class AbstractExternalModalController extends EventControllerBas return; this.doDestroyWindow(); - if(this.ipcChannel) + if(this.ipcChannel) { ipc.getInstance().deleteChannel(this.ipcChannel); + } this.destroyIPC(); this.modalState = ModalState.DESTROYED; @@ -117,6 +118,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas } protected handleWindowClosed() { + /* no other way currently */ this.destroy(); } diff --git a/shared/js/ui/tree/Controller.tsx b/shared/js/ui/tree/Controller.tsx index 1143e11e..f9163cae 100644 --- a/shared/js/ui/tree/Controller.tsx +++ b/shared/js/ui/tree/Controller.tsx @@ -19,13 +19,16 @@ 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 {spawnChannelTreePopout} from "tc-shared/ui/tree/popout/Controller"; import {server_connections} from "tc-shared/ConnectionManager"; -export function renderChannelTree(channelTree: ChannelTree, target: HTMLElement) { +export interface ChannelTreeRendererOptions { + popoutButton: boolean; +} + +export function renderChannelTree(channelTree: ChannelTree, target: HTMLElement, options: ChannelTreeRendererOptions) { const events = new Registry(); events.enableDebug("channel-tree-view"); - initializeChannelTreeController(events, channelTree); + initializeChannelTreeController(events, channelTree, options); ReactDOM.render(, target); @@ -40,10 +43,6 @@ export function renderChannelTree(channelTree: ChannelTree, target: HTMLElement) events.fire("notify_destroy"); events.destroy(); }); - - (window as any).chan_pop = () => { - spawnChannelTreePopout(channelTree.client); - } } /* FIXME: Client move is not a part of the channel tree, it's part of our own controller here */ @@ -86,6 +85,7 @@ const ClientTalkStatusUpdateKeys: (keyof ClientProperties)[] = [ class ChannelTreeController { readonly events: Registry; readonly channelTree: ChannelTree; + readonly options: ChannelTreeRendererOptions; /* the key here is the unique entry id! */ private eventListeners: {[key: number]: (() => void)[]} = {}; @@ -96,9 +96,10 @@ class ChannelTreeController { private readonly groupUpdatedListener; private readonly groupsReceivedListener; - constructor(events, channelTree) { + constructor(events, channelTree, options: ChannelTreeRendererOptions) { this.events = events; this.channelTree = channelTree; + this.options = options; this.connectionStateListener = this.handleConnectionStateChanged.bind(this); this.voiceConnectionStateListener = this.handleVoiceConnectionStateChanged.bind(this); @@ -184,6 +185,11 @@ class ChannelTreeController { } /* general channel tree event handlers */ + @EventHandler("notify_popout_state_changed") + private handlePoputStateChanged() { + this.sendPopoutState(); + } + @EventHandler("notify_channel_list_received") private handleChannelListReceived() { this.channelTreeInitialized = true; @@ -338,6 +344,13 @@ class ChannelTreeController { } /* notify state update methods */ + public sendPopoutState() { + this.events.fire_async("notify_popout_state", { + showButton: this.options.popoutButton, + shown: this.channelTree.popoutController.hasBeenPopedOut() + }); + } + public sendChannelTreeEntries() { const entries = [] as ChannelTreeEntry[]; @@ -520,13 +533,14 @@ class ChannelTreeController { } } -export function initializeChannelTreeController(events: Registry, channelTree: ChannelTree) { +export function initializeChannelTreeController(events: Registry, channelTree: ChannelTree, options: ChannelTreeRendererOptions) { /* initialize the general update handler */ - const controller = new ChannelTreeController(events, channelTree); + const controller = new ChannelTreeController(events, channelTree, options); controller.initialize(); events.on("notify_destroy", () => controller.destroy()); /* initialize the query handlers */ + events.on("query_popout_state", () => controller.sendPopoutState()); events.on("query_unread_state", event => { const entry = channelTree.findEntryId(event.treeEntryId); @@ -624,6 +638,14 @@ export function initializeChannelTreeController(events: Registry { + if(event.shown) { + channelTree.popoutController.popout(); + } else { + channelTree.popoutController.popin(); + } + }) + events.on("action_set_collapsed_state", event => { const entry = channelTree.findEntryId(event.treeEntryId); if(!entry || !(entry instanceof ChannelEntry)) { diff --git a/shared/js/ui/tree/Definitions.ts b/shared/js/ui/tree/Definitions.ts index 8eab9fb0..816d2ff4 100644 --- a/shared/js/ui/tree/Definitions.ts +++ b/shared/js/ui/tree/Definitions.ts @@ -28,6 +28,7 @@ export type ServerState = { state: "disconnected" } | { state: "connecting", tar export interface ChannelTreeUIEvents { /* actions */ + action_toggle_popout: { shown: boolean }, action_show_context_menu: { treeEntryId: number, pageX: number, pageY: number }, action_start_entry_move: { start: { x: number, y: number }, current: { x: number, y: number } }, action_set_collapsed_state: { treeEntryId: number, state: "collapsed" | "expended" }, @@ -44,6 +45,8 @@ export interface ChannelTreeUIEvents { /* queries */ query_tree_entries: {}, + query_popout_state: {}, + query_unread_state: { treeEntryId: number }, query_select_state: { treeEntryId: number }, @@ -60,6 +63,7 @@ export interface ChannelTreeUIEvents { /* notifies */ notify_tree_entries: { entries: ChannelTreeEntry[] }, + notify_popout_state: { shown: boolean, showButton: boolean }, notify_channel_info: { treeEntryId: number, info: ChannelEntryInfo }, notify_channel_icon: { treeEntryId: number, icon: ClientIcon }, diff --git a/shared/js/ui/tree/RendererDataProvider.tsx b/shared/js/ui/tree/RendererDataProvider.tsx index cf918761..e485c4b7 100644 --- a/shared/js/ui/tree/RendererDataProvider.tsx +++ b/shared/js/ui/tree/RendererDataProvider.tsx @@ -6,7 +6,7 @@ import { ClientIcons, ClientNameInfo, ClientTalkIconState, ServerState } from "tc-shared/ui/tree/Definitions"; -import {ChannelTreeView} from "tc-shared/ui/tree/RendererView"; +import {ChannelTreeView, PopoutButton} from "tc-shared/ui/tree/RendererView"; import * as React from "react"; import {ChannelIconClass, ChannelIconsRenderer, RendererChannel} from "tc-shared/ui/tree/RendererChannel"; import {ClientIcon} from "svg-sprites/client-icons"; @@ -69,6 +69,10 @@ export class RDPChannelTree { readonly refMove = React.createRef(); readonly refTree = React.createRef(); + readonly refPopoutButton = React.createRef(); + + popoutShown: boolean = false; + popoutButtonShown: boolean = false; private treeRevision: number = 0; private orderedTree: RDPEntry[] = []; @@ -198,6 +202,7 @@ export class RDPChannelTree { })); this.events.fire("query_tree_entries"); + this.events.fire("query_popout_state"); } destroy() { @@ -268,6 +273,13 @@ export class RDPChannelTree { this.refMove.current.enableEntryMove(event.entries, event.begin, event.current); } + + @EventHandler("notify_popout_state") + private handleNotifyPopoutState(event: ChannelTreeUIEvents["notify_popout_state"]) { + this.popoutShown = event.shown; + this.popoutButtonShown = event.showButton; + this.refPopoutButton.current?.forceUpdate(); + } } export abstract class RDPEntry { diff --git a/shared/js/ui/tree/RendererView.tsx b/shared/js/ui/tree/RendererView.tsx index 9459c583..a63db578 100644 --- a/shared/js/ui/tree/RendererView.tsx +++ b/shared/js/ui/tree/RendererView.tsx @@ -14,15 +14,21 @@ import {ClientIcon} from "svg-sprites/client-icons"; const viewStyle = require("./View.scss"); -const PopoutButton = (props: {}) => { - return ( -
-
- +export class PopoutButton extends React.Component<{ tree: RDPChannelTree }, {}> { + render() { + if(!this.props.tree.popoutButtonShown) { + return null; + } + + return ( +
this.props.tree.events.fire("action_toggle_popout", { shown: !this.props.tree.popoutShown })}> +
+ +
-
- ) -}; + ); + } +} export interface ChannelTreeViewProperties { events: Registry; @@ -203,7 +209,7 @@ export class ChannelTreeView extends ReactComponentBase
- +
) } diff --git a/shared/js/ui/tree/popout/Controller.ts b/shared/js/ui/tree/popout/Controller.ts index 84634337..35a0e2a1 100644 --- a/shared/js/ui/tree/popout/Controller.ts +++ b/shared/js/ui/tree/popout/Controller.ts @@ -1,4 +1,3 @@ -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"; @@ -7,35 +6,114 @@ 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"; +import {ChannelTree} from "tc-shared/tree/ChannelTree"; +import {ModalController} from "tc-shared/ui/react-elements/ModalDefinitions"; +import {ChannelTreePopoutEvents} from "tc-shared/ui/tree/popout/Definitions"; +import {ConnectionState} from "tc-shared/ConnectionHandler"; -export function spawnChannelTreePopout(handler: ConnectionHandler) { - const eventsTree = new Registry(); - eventsTree.enableDebug("channel-tree-view-modal"); - initializeChannelTreeController(eventsTree, handler.channelTree); +export class ChannelTreePopoutController { + readonly channelTree: ChannelTree; - const eventsControlBar = new Registry(); - initializePopoutControlBarController(eventsControlBar, handler); + private popoutInstance: ModalController; + private uiEvents: Registry; + private treeEvents: Registry; + private controlBarEvents: Registry; - let handlerDestroyListener; - server_connections.events().on("notify_handler_deleted", handlerDestroyListener = event => { - if(event.handler !== handler) { + private generalEvents: (() => void)[]; + + constructor(channelTree: ChannelTree) { + this.channelTree = channelTree; + + this.generalEvents = []; + this.generalEvents.push(this.channelTree.server.events.on("notify_properties_updated", event => { + if("virtualserver_name" in event.updated_properties) { + this.sendTitle(); + } + })); + + this.generalEvents.push(this.channelTree.client.events().on("notify_connection_state_changed", () => this.sendTitle())); + } + + destroy() { + this.popin(); + this.generalEvents?.forEach(callback => callback()); + this.generalEvents = undefined; + } + + hasBeenPopedOut() { + return !!this.popoutInstance; + } + + popout() { + if(this.popoutInstance) { + /* TODO: Request focus on that window? */ return; } - modal.destroy(); - }); + this.uiEvents = new Registry(); + this.uiEvents.on("query_title", () => this.sendTitle()); - const modal = spawnExternalModal("channel-tree", { tree: eventsTree, controlBar: eventsControlBar }, { handlerId: handler.handlerId }, "channel-tree-" + handler.handlerId); - modal.show(); + this.treeEvents = new Registry(); + initializeChannelTreeController(this.treeEvents, this.channelTree, { popoutButton: false }); - modal.getEvents().on("destroy", () => { - server_connections.events().off("notify_handler_deleted", handlerDestroyListener); + this.controlBarEvents = new Registry(); + initializePopoutControlBarController(this.controlBarEvents, this.channelTree.client); - eventsTree.fire("notify_destroy"); - eventsTree.destroy(); + this.popoutInstance = spawnExternalModal("channel-tree", { + tree: this.treeEvents, + controlBar: this.controlBarEvents, + base: this.uiEvents + }, { handlerId: this.channelTree.client.handlerId }, "channel-tree-" + this.channelTree.client.handlerId); - eventsControlBar.fire("notify_destroy"); - eventsControlBar.destroy(); - }); + this.popoutInstance.getEvents().one("destroy", () => { + this.treeEvents.fire("notify_destroy"); + this.treeEvents.destroy(); + this.treeEvents = undefined; + + this.controlBarEvents.fire("notify_destroy"); + this.controlBarEvents.destroy(); + this.controlBarEvents = undefined; + + this.uiEvents.destroy(); + this.uiEvents = undefined; + + this.popoutInstance = undefined; + this.channelTree.events.fire("notify_popout_state_changed", { popoutShown: false }); + }); + this.popoutInstance.show(); + + this.channelTree.events.fire("notify_popout_state_changed", { popoutShown: true }); + } + + popin() { + if(!this.popoutInstance) { return; } + + this.popoutInstance.destroy(); + this.popoutInstance = undefined; /* not needed, but just to ensure (will be set within the destroy callback already) */ + } + + private sendTitle() { + if(!this.uiEvents) { return; } + + let title; + switch (this.channelTree.client.connection_state) { + case ConnectionState.INITIALISING: + case ConnectionState.CONNECTING: + case ConnectionState.AUTHENTICATING: + const address = this.channelTree.server.remote_address; + title = tra("Connecting to {}", address.host + (address.port === 9987 ? "" : `:${address.port}`)); + break; + + case ConnectionState.DISCONNECTING: + case ConnectionState.UNCONNECTED: + title = tr("Not connected"); + break; + + case ConnectionState.CONNECTED: + title = this.channelTree.server.properties.virtualserver_name; + break; + } + + this.uiEvents.fire_async("notify_title", { title: title }); + } } \ No newline at end of file diff --git a/shared/js/ui/tree/popout/Definitions.ts b/shared/js/ui/tree/popout/Definitions.ts new file mode 100644 index 00000000..22e52813 --- /dev/null +++ b/shared/js/ui/tree/popout/Definitions.ts @@ -0,0 +1,4 @@ +export interface ChannelTreePopoutEvents { + query_title: {}, + notify_title: { title: string } +} \ No newline at end of file diff --git a/shared/js/ui/tree/popout/RendererModal.tsx b/shared/js/ui/tree/popout/RendererModal.tsx index ff3a5fce..a034b129 100644 --- a/shared/js/ui/tree/popout/RendererModal.tsx +++ b/shared/js/ui/tree/popout/RendererModal.tsx @@ -2,13 +2,25 @@ 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"; +import {ChannelTreePopoutEvents} from "tc-shared/ui/tree/popout/Definitions"; +import {useState} from "react"; + +const TitleRenderer = (props: { events: Registry }) => { + const [ title, setTitle ] = useState(() => { + props.events.fire("query_title"); + return tr("Channel tree popout"); + }); + + props.events.reactUse("notify_title", event => setTitle(event.title)); + return <>{title}; +} const cssStyle = require("./RendererModal.scss"); class ChannelTreeModal extends AbstractModal { + readonly eventsUI: Registry; readonly eventsTree: Registry; readonly eventsControlBar: Registry; @@ -18,8 +30,15 @@ class ChannelTreeModal extends AbstractModal { super(); this.handlerId = userData.handlerId; + this.eventsUI = registryMap["base"] as any; this.eventsTree = registryMap["tree"] as any; this.eventsControlBar = registryMap["controlBar"] as any; + + this.eventsUI.fire("query_title"); + } + + protected onDestroy() { + super.onDestroy(); } renderBody(): React.ReactElement { @@ -35,8 +54,8 @@ class ChannelTreeModal extends AbstractModal { ) } - title(): string | React.ReactElement { - return Channel tree; + title(): React.ReactElement { + return ; } } diff --git a/web/app/voice/VoiceHandler.ts b/web/app/voice/VoiceHandler.ts index e72ee407..fdc50751 100644 --- a/web/app/voice/VoiceHandler.ts +++ b/web/app/voice/VoiceHandler.ts @@ -542,7 +542,7 @@ export class VoiceConnection extends AbstractVoiceConnection { logWarn(LogCategory.CLIENT, tr("Failed to clear the whisper target: %o"), error); }); } - this.voiceBridge.stopWhispering(); + this.voiceBridge?.stopWhispering(); } }