diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 9d5da2ae..b1ad4dc7 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -8,7 +8,6 @@ import {LogCategory, logError, logInfo, logTrace, logWarn} from "./log"; import {createErrorModal, createInputModal, Modal} from "./ui/elements/Modal"; import {hashPassword} from "./utils/helpers"; import {HandshakeHandler} from "./connection/HandshakeHandler"; -import * as htmltags from "./ui/htmltags"; import {FilterMode, InputStartError, InputState} from "./voice/RecorderBase"; import {defaultRecorder, RecorderProfile} from "./voice/RecorderProfile"; import {Regex} from "./ui/modal/ModalConnect"; @@ -38,6 +37,7 @@ import {ConnectParameters} from "tc-shared/ui/modal/connect/Controller"; import {assertMainApplication} from "tc-shared/ui/utils"; import {getDNSProvider} from "tc-shared/dns"; import {W2GPluginCmdHandler} from "tc-shared/ui/modal/video-viewer/W2GPlugin"; +import * as htmltags from "./ui/htmltags"; assertMainApplication(); @@ -1249,11 +1249,6 @@ export interface ConnectionEvents { newState: ConnectionState }, - /* the handler has become visible/invisible for the client */ - notify_visibility_changed: { - visible: boolean - }, - /* fill only trigger once, after everything has been constructed */ notify_handler_initialized: {} } \ No newline at end of file diff --git a/shared/js/ConnectionManager.ts b/shared/js/ConnectionManager.ts index 2bcb79cb..b2db395d 100644 --- a/shared/js/ConnectionManager.ts +++ b/shared/js/ConnectionManager.ts @@ -102,8 +102,6 @@ export class ConnectionManager { oldHandlerId: oldHandler?.handlerId, newHandlerId: handler?.handlerId }); - oldHandler?.events().fire("notify_visibility_changed", { visible: false }); - handler?.events().fire("notify_visibility_changed", { visible: true }); } swapHandlerOrder(handlerA: ConnectionHandler, handlerB: ConnectionHandler) { diff --git a/shared/js/ui/AppRenderer.tsx b/shared/js/ui/AppRenderer.tsx index 5402f553..13dfec4d 100644 --- a/shared/js/ui/AppRenderer.tsx +++ b/shared/js/ui/AppRenderer.tsx @@ -19,6 +19,7 @@ import {AppUiEvents} from "tc-shared/ui/AppDefinitions"; import {ChannelTreeRenderer} from "tc-shared/ui/tree/Renderer"; import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions"; import {ImagePreviewHook} from "tc-shared/ui/frames/ImagePreview"; +import {InternalModalHook} from "tc-shared/ui/react-elements/modal/internal"; const cssStyle = require("./AppRenderer.scss"); const VideoFrame = React.memo((props: { events: Registry }) => { @@ -105,6 +106,10 @@ export const TeaAppMainView = (props: { + + + + ); } \ No newline at end of file diff --git a/shared/js/ui/frames/side/AbstractConversationController.ts b/shared/js/ui/frames/side/AbstractConversationController.ts index d86d3eef..e7827452 100644 --- a/shared/js/ui/frames/side/AbstractConversationController.ts +++ b/shared/js/ui/frames/side/AbstractConversationController.ts @@ -110,11 +110,6 @@ export abstract class AbstractConversationController< } } - /* TODO: Is this even a thing? */ - handlePanelShow() { - this.uiEvents.fire_react("notify_panel_show"); - } - protected reportStateToUI(conversation: AbstractChat) { let historyState: ChatHistoryState; const localHistoryState = this.conversationManager.historyUiStates[conversation.getChatId()]; diff --git a/shared/js/ui/frames/side/AbstractConversationDefinitions.ts b/shared/js/ui/frames/side/AbstractConversationDefinitions.ts index a396fca0..5daf8b25 100644 --- a/shared/js/ui/frames/side/AbstractConversationDefinitions.ts +++ b/shared/js/ui/frames/side/AbstractConversationDefinitions.ts @@ -139,7 +139,6 @@ export interface AbstractConversationUiEvents { } notify_selected_chat: { chatId: "unselected" | string }, - notify_panel_show: {}, notify_chat_event: { chatId: string, triggerUnread: boolean, diff --git a/shared/js/ui/frames/side/AbstractConversationRenderer.tsx b/shared/js/ui/frames/side/AbstractConversationRenderer.tsx index b841707e..114effb5 100644 --- a/shared/js/ui/frames/side/AbstractConversationRenderer.tsx +++ b/shared/js/ui/frames/side/AbstractConversationRenderer.tsx @@ -824,11 +824,6 @@ class ConversationMessages extends React.PureComponent("notify_panel_show") - private handlePanelShow() { - this.fixScroll(); - } - @EventHandler("query_conversation_history") private handleQueryConversationHistory(event: AbstractConversationUiEvents["query_conversation_history"]) { if (event.chatId !== this.currentChatId) diff --git a/shared/js/ui/frames/side/ChannelConversationController.ts b/shared/js/ui/frames/side/ChannelConversationController.ts index 9b017237..58a027d4 100644 --- a/shared/js/ui/frames/side/ChannelConversationController.ts +++ b/shared/js/ui/frames/side/ChannelConversationController.ts @@ -57,7 +57,6 @@ export class ChannelConversationController extends AbstractConversationControlle this.connection = connection; if(connection) { - this.initializeConnectionListener(connection); /* FIXME: Update cross channel talk state! */ this.setConversationManager(connection.getChannelConversations()); } else { @@ -65,16 +64,6 @@ export class ChannelConversationController extends AbstractConversationControlle } } - private initializeConnectionListener(connection: ConnectionHandler) { - this.connectionListener.push(connection.events().on("notify_visibility_changed", event => { - if(!event.visible) { - return; - } - - this.handlePanelShow(); - })); - } - @EventHandler("action_delete_message") private handleMessageDelete(event: AbstractConversationUiEvents["action_delete_message"]) { const conversation = this.conversationManager?.findConversationById(event.chatId); diff --git a/shared/js/ui/frames/side/PrivateConversationController.ts b/shared/js/ui/frames/side/PrivateConversationController.ts index 714b3e31..776d69fb 100644 --- a/shared/js/ui/frames/side/PrivateConversationController.ts +++ b/shared/js/ui/frames/side/PrivateConversationController.ts @@ -73,7 +73,6 @@ export class PrivateConversationController extends AbstractConversationControlle this.connection = connection; if(connection) { - this.initializeConnectionListener(connection); this.setConversationManager(connection.getPrivateConversations()); } else { this.setConversationManager(undefined); @@ -81,15 +80,6 @@ export class PrivateConversationController extends AbstractConversationControlle this.reportConversationList(); } - private initializeConnectionListener(connection: ConnectionHandler) { - this.connectionListener.push(connection.events().on("notify_visibility_changed", event => { - if(!event.visible) - return; - - this.handlePanelShow(); - })); - } - protected registerConversationManagerEvents(manager: PrivateConversationManager) { super.registerConversationManagerEvents(manager); diff --git a/shared/js/ui/react-elements/modal/Controller.ts b/shared/js/ui/react-elements/modal/Controller.ts index 3597fc73..731586e3 100644 --- a/shared/js/ui/react-elements/modal/Controller.ts +++ b/shared/js/ui/react-elements/modal/Controller.ts @@ -13,6 +13,7 @@ import {findRegisteredModal, RegisteredModal} from "tc-shared/ui/react-elements/ import {assertMainApplication} from "tc-shared/ui/utils"; import {InternalModalInstance} from "./internal"; import {ExternalModalController} from "./external/Controller"; +import {LogCategory, logError} from "tc-shared/log"; assertMainApplication(); export class GenericModalController implements ModalController { @@ -82,7 +83,15 @@ export class GenericModalController i } }); - events.on("action_close", () => this.destroy()); + events.on("action_close", () => { + if(this.popedOut) { + this.destroy(); + } else { + this.hide().catch(error => { + logError(LogCategory.GENERAL, tr("Failed to hide modal: %o"), error); + }).then(() => this.destroy()); + } + }); events.on("action_minimize", () => this.instance.minimize()); events.on("action_popout", () => { diff --git a/shared/js/ui/react-elements/modal/Renderer.tsx b/shared/js/ui/react-elements/modal/Renderer.tsx index c25a89a7..bd3cf7fc 100644 --- a/shared/js/ui/react-elements/modal/Renderer.tsx +++ b/shared/js/ui/react-elements/modal/Renderer.tsx @@ -116,7 +116,9 @@ export class ModalBodyRenderer extends React.PureComponent<{ this.props.className, cssStyle["color-" + this.props.modalInstance.color()] )}> - {this.props.modalInstance.renderBody()} + + {this.props.modalInstance.renderBody()} + ) } @@ -139,8 +141,8 @@ export class ModalFrameRenderer extends React.PureComponent<{ export class PageModalRenderer extends React.PureComponent<{ modalInstance: AbstractModal, onBackdropClicked: () => void, - children: React.ReactElement -}, { + children: React.ReactElement, + shown: boolean }> { constructor(props) { @@ -157,7 +159,7 @@ export class PageModalRenderer extends React.PureComponent<{ className={joinClassList( cssStyle.modalPageContainer, cssStyle["align-" + this.props.modalInstance.verticalAlignment()], - this.state.shown ? cssStyle.shown : undefined + this.props.shown ? cssStyle.shown : undefined )} tabIndex={-1} role={"dialog"} diff --git a/shared/js/ui/react-elements/modal/internal/index.tsx b/shared/js/ui/react-elements/modal/internal/index.tsx index dfa516dc..5fe61ed9 100644 --- a/shared/js/ui/react-elements/modal/internal/index.tsx +++ b/shared/js/ui/react-elements/modal/internal/index.tsx @@ -1,11 +1,12 @@ import { AbstractModal, - constructAbstractModalClass, ModalInstanceController, ModalInstanceEvents, + constructAbstractModalClass, + ModalInstanceController, + ModalInstanceEvents, ModalOptions, ModalState } from "tc-shared/ui/react-elements/modal/Definitions"; import * as React from "react"; -import * as ReactDOM from "react-dom"; import { ModalBodyRenderer, ModalFrameRenderer, @@ -15,36 +16,79 @@ import { import {RegisteredModal} from "tc-shared/ui/react-elements/modal/Registry"; import {LogCategory, logError} from "tc-shared/log"; import {Registry} from "tc-events"; +import {guid} from "tc-shared/crypto/uid"; +import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary"; + +class InternalRendererInstance extends React.PureComponent<{ + instance: InternalModalInstance, +}, { + shown: boolean +}> { + constructor(props) { + super(props); + + this.state = { + shown: false + }; + } + + render() { + const instance = this.props.instance; + if(!instance?.modalInstance) { + throw tr("missing modal instance"); + } + + return ( + + + + + + + ); + } + + componentWillUnmount() { + /* TODO: May notify the instance about this if this wasn't planned */ + } +} export class InternalModalInstance implements ModalInstanceController { + readonly instanceUniqueId: string; readonly events: Registry; + readonly refRendererInstance: React.RefObject; private readonly modalKlass: RegisteredModal; private readonly constructorArguments: any[]; - private readonly rendererInstance: React.RefObject; private readonly modalOptions: ModalOptions; private state: ModalState; - private modalInstance: AbstractModal; - private htmlContainer: HTMLDivElement; - + public modalInstance: AbstractModal; private modalInitializePromise: Promise; constructor(modalType: RegisteredModal, constructorArguments: any[], modalOptions: ModalOptions) { + this.instanceUniqueId = guid(); this.events = new Registry(); this.modalKlass = modalType; this.modalOptions = modalOptions; this.constructorArguments = constructorArguments; - this.rendererInstance = React.createRef(); + this.refRendererInstance = React.createRef(); this.state = ModalState.DESTROYED; } private async constructModal() { - if(this.htmlContainer || this.modalInstance) { + if(this.modalInstance) { throw tr("internal modal has already been constructed"); } @@ -57,47 +101,41 @@ export class InternalModalInstance implements ModalInstanceController { this.modalInstance = constructAbstractModalClass(modalClass.default, { windowed: false }, this.constructorArguments); this.modalInstance["onInitialize"](); } catch (error) { + this.destructModalInstance(); logError(LogCategory.GENERAL, tr("Failed to create new modal of instance type %s: %o"), this.modalKlass.modalId, error); throw tr("failed to create new modal instance"); } - this.htmlContainer = document.createElement("div"); - document.body.appendChild(this.htmlContainer); + if(!internalModalContainer.current) { + this.destructModalInstance(); + throw tr("missing modal hanging container"); + } - await new Promise(resolve => { - ReactDOM.render( - - - - - - , - this.htmlContainer, - resolve - ); - }); + await new Promise(resolve => internalModalContainer.current.addModalInstance(this, resolve)); + if(!this.refRendererInstance.current) { + this.destructModalInstance(); + throw tr("missing rendered modal reference"); + } } private destructModal() { this.state = ModalState.DESTROYED; - if(this.htmlContainer) { - ReactDOM.unmountComponentAtNode(this.htmlContainer); - this.htmlContainer.remove(); - this.htmlContainer = undefined; + this.destructModalInstance(); + this.events.fire("notify_destroy"); + } + + private destructModalInstance() { + internalModalContainer.current?.removeModalInstance(this); + if(!this.modalInstance) { + return; } - if(this.modalInstance) { + try { this.modalInstance["onDestroy"](); - this.modalInstance = undefined; + } catch (error) { + logError(LogCategory.GENERAL, tr("Failed to invoke the destroy callback on the created modal instance: %o"), error); } - this.events.fire("notify_destroy"); + this.modalInstance = undefined; } getState(): ModalState { @@ -118,13 +156,13 @@ export class InternalModalInstance implements ModalInstanceController { await this.modalInitializePromise; } - if(!this.rendererInstance.current) { + if(!this.refRendererInstance.current) { return; } - this.state = ModalState.SHOWN; this.modalInstance["onOpen"](); - await new Promise(resolve => this.rendererInstance.current.setState({ shown: true }, resolve)); + this.state = ModalState.SHOWN; + await new Promise(resolve => this.refRendererInstance.current.setState({ shown: true }, resolve)); this.events.fire("notify_open"); } @@ -133,13 +171,13 @@ export class InternalModalInstance implements ModalInstanceController { await this.modalInitializePromise; } - if(!this.rendererInstance.current) { + if(!this.refRendererInstance.current) { return; } this.state = ModalState.HIDDEN; this.modalInstance["onClose"](); - await new Promise(resolve => this.rendererInstance.current.setState({ shown: false }, resolve)); + await new Promise(resolve => this.refRendererInstance.current.setState({ shown: false }, resolve)); /* TODO: Somehow get the real animation finish signal? */ await new Promise(resolve => setTimeout(resolve, 500)); @@ -154,11 +192,11 @@ export class InternalModalInstance implements ModalInstanceController { this.events.destroy(); } - protected getCloseCallback() { + public getCloseCallback() { return () => this.events.fire("action_close"); } - protected getPopoutCallback() { + public getPopoutCallback() { if(!this.modalKlass.popoutSupported) { return undefined; } @@ -170,8 +208,48 @@ export class InternalModalInstance implements ModalInstanceController { return () => this.events.fire("action_popout"); } - protected getMinimizeCallback() { + public getMinimizeCallback() { /* We can't minimize any windows */ return undefined; } -} \ No newline at end of file +} + +const internalModalContainer: React.RefObject = React.createRef(); +class InternalModalHookInner extends React.PureComponent<{}, { + revision: number +}> { + private modalStack: InternalModalInstance[]; + + constructor(props) { + super(props); + + this.modalStack = []; + this.state = { revision: 0 }; + } + + render() { + return ( + this.modalStack.map(modal => ( + + + + )) + ); + } + + addModalInstance(modal: InternalModalInstance, callbackRendered?: () => void) { + this.modalStack.push(modal); + this.setState({ revision: performance.now() }, callbackRendered); + } + + removeModalInstance(modal: InternalModalInstance) { + if(!this.modalStack.remove(modal)) { + return; + } + this.setState({ revision: performance.now() }); + } +} + +export const InternalModalHook = React.memo(() => ( + +)); \ No newline at end of file diff --git a/shared/js/ui/tree/Controller.tsx b/shared/js/ui/tree/Controller.tsx index 079b83c3..d6fe58cf 100644 --- a/shared/js/ui/tree/Controller.tsx +++ b/shared/js/ui/tree/Controller.tsx @@ -680,8 +680,6 @@ export function initializeChannelTreeController(events: Registry events.fire_react("notify_visibility_changed", event))); - events.on("query_tree_entries", event => controller.sendChannelTreeEntriesFull(event.fullInfo ? undefined : [])); events.on("query_selected_entry", () => controller.sendSelectedEntry()); events.on("query_channel_info", event => { diff --git a/shared/js/ui/tree/Definitions.ts b/shared/js/ui/tree/Definitions.ts index b56b162c..be38ff72 100644 --- a/shared/js/ui/tree/Definitions.ts +++ b/shared/js/ui/tree/Definitions.ts @@ -103,7 +103,6 @@ export interface ChannelTreeUIEvents { notify_unread_state: { treeEntryId: number, unread: boolean }, - notify_visibility_changed: { visible: boolean }, notify_destroy: {} } diff --git a/shared/js/ui/tree/RendererView.tsx b/shared/js/ui/tree/RendererView.tsx index a690a886..21ef9af3 100644 --- a/shared/js/ui/tree/RendererView.tsx +++ b/shared/js/ui/tree/RendererView.tsx @@ -110,25 +110,6 @@ export class ChannelTreeView extends ReactComponentBase("notify_visibility_changed") - private handleVisibilityChanged(event: ChannelTreeUIEvents["notify_visibility_changed"]) { - if (!event.visible) { - this.setState({smoothScroll: false}); - return; - } - - if (this.scrollFixRequested) { - return; - } - - this.scrollFixRequested = true; - requestAnimationFrame(() => { - this.scrollFixRequested = false; - this.refContainer.current.scrollTop = this.state.scrollOffset; - this.setState({smoothScroll: true}); - }); - } - private visibleEntries() { const entryHeight = ChannelTreeView.EntryHeightEm * this.state.fontSize; let viewEntryCount = Math.ceil(this.state.viewHeight / entryHeight); diff --git a/shared/svg-sprites/client-icons.d.ts b/shared/svg-sprites/client-icons.d.ts index a12d497b..d26992a5 100644 --- a/shared/svg-sprites/client-icons.d.ts +++ b/shared/svg-sprites/client-icons.d.ts @@ -3,9 +3,9 @@ * * This file has been auto generated by the svg-sprite generator. * Sprite source directory: G:\TeaSpeak\web\shared\img\client-icons - * Sprite count: 221 + * Sprite count: 222 */ -export type ClientIconClass = "client-about" | "client-activate_microphone" | "client-add" | "client-add_foe" | "client-add_folder" | "client-add_friend" | "client-addon-collection" | "client-addon" | "client-apply" | "client-arrow_down" | "client-arrow_left" | "client-arrow_right" | "client-arrow_up" | "client-avatar_upload" | "client-away" | "client-ban_client" | "client-ban_list" | "client-bookmark_add" | "client-bookmark_add_folder" | "client-bookmark_duplicate" | "client-bookmark_edit_name" | "client-bookmark_manager" | "client-bookmark_remove" | "client-broken_image" | "client-browse-addon-online" | "client-capture" | "client-change_nickname" | "client-changelog" | "client-channel_chat" | "client-channel_collapse_all" | "client-channel_commander" | "client-channel_create" | "client-channel_create_sub" | "client-channel_default" | "client-channel_delete" | "client-channel_edit" | "client-channel_expand_all" | "client-channel_green" | "client-channel_green_subscribed" | "client-channel_green_subscribed2" | "client-channel_popin" | "client-channel_popout" | "client-channel_private" | "client-channel_red" | "client-channel_red_subscribed" | "client-channel_switch" | "client-channel_unsubscribed" | "client-channel_yellow" | "client-channel_yellow_subscribed" | "client-check_update" | "client-client_info_country" | "client-client_info_forum_account" | "client-client_info_online_time" | "client-client_info_status" | "client-client_info_version" | "client-client_info_volume" | "client-close_button" | "client-complaint_list" | "client-conflict-icon" | "client-connect" | "client-contact" | "client-copy" | "client-copy_url" | "client-d_sound" | "client-d_sound_me" | "client-d_sound_user" | "client-default" | "client-default_for_all_bookmarks" | "client-delete" | "client-delete_avatar" | "client-disconnect" | "client-double_arrow" | "client-down" | "client-download" | "client-edit" | "client-edit_friend_foe_status" | "client-emoticon" | "client-error" | "client-file_home" | "client-file_refresh" | "client-filetransfer" | "client-find" | "client-folder" | "client-folder_up" | "client-fullscreen" | "client-group_100" | "client-group_200" | "client-group_300" | "client-group_500" | "client-group_600" | "client-group_add" | "client-group_delete" | "client-group_permission_copy" | "client-group_rename" | "client-guisetup" | "client-hardware_input_muted" | "client-hardware_output_muted" | "client-home" | "client-hoster_button" | "client-hotkeys" | "client-icon-pack" | "client-iconsview" | "client-iconviewer" | "client-identity_default" | "client-identity_export" | "client-identity_import" | "client-identity_manager" | "client-image_preview_browse" | "client-info" | "client-input_muted" | "client-input_muted_local" | "client-invite_buddy" | "client-is_talker" | "client-kick_channel" | "client-kick_server" | "client-listview" | "client-loading_image" | "client-message_incoming" | "client-message_info" | "client-message_outgoing" | "client-messages" | "client-microphone_broken" | "client-minimize_button" | "client-moderated" | "client-move_client_to_own_channel" | "client-music" | "client-new_chat" | "client-notifications" | "client-offline_messages" | "client-on_whisperlist" | "client-output_muted" | "client-permission_channel" | "client-permission_client" | "client-permission_overview" | "client-permission_server_groups" | "client-phoneticsnickname" | "client-ping_1" | "client-ping_2" | "client-ping_3" | "client-ping_4" | "client-ping_calculating" | "client-ping_disconnected" | "client-play" | "client-player_chat" | "client-player_commander_off" | "client-player_commander_on" | "client-player_off" | "client-player_on" | "client-player_whisper" | "client-plugins" | "client-poke" | "client-present" | "client-recording_start" | "client-recording_stop" | "client-refresh" | "client-register" | "client-reload" | "client-remove_foe" | "client-remove_friend" | "client-security" | "client-selectfolder" | "client-send_complaint" | "client-server_green" | "client-server_log" | "client-server_query" | "client-settings" | "client-share_screen" | "client-simple_arrow" | "client-sort_by_name" | "client-sound-pack" | "client-soundpack" | "client-stop" | "client-subscribe_mode" | "client-subscribe_to_all_channels" | "client-subscribe_to_channel" | "client-subscribe_to_channel_family" | "client-switch_advanced" | "client-switch_standard" | "client-sync-disable" | "client-sync-enable" | "client-sync-icon" | "client-tab_close_button" | "client-talk_power_grant" | "client-talk_power_grant_next" | "client-talk_power_request" | "client-talk_power_request_cancel" | "client-talk_power_revoke" | "client-talk_power_revoke_all_grant_next" | "client-teaspeak_logo" | "client-temp_server_password" | "client-temp_server_password_add" | "client-textformat" | "client-textformat_bold" | "client-textformat_foreground" | "client-textformat_italic" | "client-textformat_underline" | "client-theme" | "client-toggle_server_query_clients" | "client-toggle_whisper" | "client-token" | "client-token_use" | "client-translation" | "client-unsubscribe_from_all_channels" | "client-unsubscribe_from_channel_family" | "client-unsubscribe_mode" | "client-up" | "client-upload" | "client-upload_avatar" | "client-urlcatcher" | "client-user-account" | "client-video_muted" | "client-virtualserver_edit" | "client-volume" | "client-w2g" | "client-warning" | "client-warning_external_link" | "client-warning_info" | "client-warning_question" | "client-weblist" | "client-whisper" | "client-whisperlists"; +export type ClientIconClass = "client-about" | "client-activate_microphone" | "client-add" | "client-add_foe" | "client-add_folder" | "client-add_friend" | "client-addon-collection" | "client-addon" | "client-apply" | "client-arrow_down" | "client-arrow_left" | "client-arrow_right" | "client-arrow_up" | "client-avatar_upload" | "client-away" | "client-ban_client" | "client-ban_list" | "client-bookmark_add" | "client-bookmark_add_folder" | "client-bookmark_duplicate" | "client-bookmark_edit_name" | "client-bookmark_manager" | "client-bookmark_remove" | "client-broken_image" | "client-browse-addon-online" | "client-capture" | "client-change_nickname" | "client-changelog" | "client-channel_chat" | "client-channel_collapse_all" | "client-channel_commander" | "client-channel_create" | "client-channel_create_sub" | "client-channel_default" | "client-channel_delete" | "client-channel_edit" | "client-channel_expand_all" | "client-channel_green" | "client-channel_green_subscribed" | "client-channel_green_subscribed2" | "client-channel_popin" | "client-channel_popout" | "client-channel_private" | "client-channel_red" | "client-channel_red_subscribed" | "client-channel_switch" | "client-channel_unsubscribed" | "client-channel_yellow" | "client-channel_yellow_subscribed" | "client-check_update" | "client-client_info_country" | "client-client_info_forum_account" | "client-client_info_online_time" | "client-client_info_status" | "client-client_info_version" | "client-client_info_volume" | "client-close_button" | "client-complaint_list" | "client-conflict-icon" | "client-connect" | "client-contact" | "client-copy" | "client-copy_url" | "client-d_sound" | "client-d_sound_me" | "client-d_sound_user" | "client-default" | "client-default_for_all_bookmarks" | "client-delete" | "client-delete_avatar" | "client-disconnect" | "client-double_arrow" | "client-down" | "client-download" | "client-edit" | "client-edit_friend_foe_status" | "client-emoticon" | "client-error" | "client-file_home" | "client-file_refresh" | "client-filetransfer" | "client-find" | "client-folder" | "client-folder_up" | "client-fullscreen" | "client-group_100" | "client-group_200" | "client-group_300" | "client-group_500" | "client-group_600" | "client-group_add" | "client-group_delete" | "client-group_permission_copy" | "client-group_rename" | "client-guisetup" | "client-hardware_input_muted" | "client-hardware_output_muted" | "client-home" | "client-hoster_button" | "client-hotkeys" | "client-icon-pack" | "client-iconsview" | "client-iconviewer" | "client-identity_default" | "client-identity_export" | "client-identity_import" | "client-identity_manager" | "client-image_preview_browse" | "client-info" | "client-input_muted" | "client-input_muted_local" | "client-invite_buddy" | "client-is_talker" | "client-kick_channel" | "client-kick_server" | "client-listview" | "client-loading_image" | "client-message_incoming" | "client-message_info" | "client-message_outgoing" | "client-messages" | "client-microphone_broken" | "client-minimize_button" | "client-moderated" | "client-move_client_to_own_channel" | "client-music" | "client-new_chat" | "client-notifications" | "client-offline_messages" | "client-on_whisperlist" | "client-output_muted" | "client-permission_channel" | "client-permission_client" | "client-permission_overview" | "client-permission_server_groups" | "client-phoneticsnickname" | "client-ping_1" | "client-ping_2" | "client-ping_3" | "client-ping_4" | "client-ping_calculating" | "client-ping_disconnected" | "client-play" | "client-player_chat" | "client-player_commander_off" | "client-player_commander_on" | "client-player_off" | "client-player_on" | "client-player_whisper" | "client-plugins" | "client-poke" | "client-present" | "client-recording_start" | "client-recording_stop" | "client-refresh" | "client-register" | "client-reload" | "client-remove_foe" | "client-remove_friend" | "client-security" | "client-selectfolder" | "client-send_complaint" | "client-server_green" | "client-server_log" | "client-server_query" | "client-settings" | "client-settings_loading" | "client-share_screen" | "client-simple_arrow" | "client-sort_by_name" | "client-sound-pack" | "client-soundpack" | "client-stop" | "client-subscribe_mode" | "client-subscribe_to_all_channels" | "client-subscribe_to_channel" | "client-subscribe_to_channel_family" | "client-switch_advanced" | "client-switch_standard" | "client-sync-disable" | "client-sync-enable" | "client-sync-icon" | "client-tab_close_button" | "client-talk_power_grant" | "client-talk_power_grant_next" | "client-talk_power_request" | "client-talk_power_request_cancel" | "client-talk_power_revoke" | "client-talk_power_revoke_all_grant_next" | "client-teaspeak_logo" | "client-temp_server_password" | "client-temp_server_password_add" | "client-textformat" | "client-textformat_bold" | "client-textformat_foreground" | "client-textformat_italic" | "client-textformat_underline" | "client-theme" | "client-toggle_server_query_clients" | "client-toggle_whisper" | "client-token" | "client-token_use" | "client-translation" | "client-unsubscribe_from_all_channels" | "client-unsubscribe_from_channel_family" | "client-unsubscribe_mode" | "client-up" | "client-upload" | "client-upload_avatar" | "client-urlcatcher" | "client-user-account" | "client-video_muted" | "client-virtualserver_edit" | "client-volume" | "client-w2g" | "client-warning" | "client-warning_external_link" | "client-warning_info" | "client-warning_question" | "client-weblist" | "client-whisper" | "client-whisperlists"; export enum ClientIcon { About = "client-about", @@ -174,6 +174,7 @@ export enum ClientIcon { ServerLog = "client-server_log", ServerQuery = "client-server_query", Settings = "client-settings", + SettingsLoading = "client-settings_loading", ShareScreen = "client-share_screen", SimpleArrow = "client-simple_arrow", SortByName = "client-sort_by_name",