From 1b2de3ed631250a818dfbd2752127c89201c9874 Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Tue, 16 Mar 2021 15:14:03 +0100 Subject: [PATCH] Added the possibility to popout modals and finished the work on the bookmark modals --- shared/js/tree/EntryTagsHandler.ts | 25 ++ shared/js/tree/Server.ts | 12 + shared/js/ui/modal/ModalChangeVolumeNew.tsx | 2 +- shared/js/ui/modal/ModalGroupCreate.tsx | 2 +- .../js/ui/modal/ModalGroupPermissionCopy.tsx | 2 +- .../modal/bookmarks-add-server/Controller.ts | 131 ++++++++- .../modal/bookmarks-add-server/Definitions.ts | 23 +- .../modal/bookmarks-add-server/Renderer.scss | 61 +++++ .../modal/bookmarks-add-server/Renderer.tsx | 159 ++++++++++- shared/js/ui/modal/bookmarks/Renderer.tsx | 10 +- shared/js/ui/modal/channel-edit/Renderer.tsx | 2 +- shared/js/ui/modal/connect/Renderer.tsx | 5 - shared/js/ui/modal/invite/Renderer.tsx | 11 +- .../permission/ModalPermissionEditor.tsx | 2 +- .../transfer/FileBrowserControllerRemote.ts | 2 +- .../transfer/FileTransferInfoController.ts | 2 +- .../ui/modal/transfer/ModalFileTransfer.tsx | 2 +- shared/js/ui/modal/video-source/Renderer.tsx | 2 +- shared/js/ui/modal/whats-new/Controller.tsx | 2 +- shared/js/ui/react-elements/InputField.tsx | 3 + shared/js/ui/react-elements/Tooltip.tsx | 11 +- .../external-modal/Controller.ts | 20 +- .../external-modal/ModalRenderer.scss | 20 ++ .../external-modal/ModalRenderer.tsx | 64 +++++ .../external-modal/PopoutEntrypoint.ts | 28 +- .../external-modal/PopoutRenderer.scss | 57 ---- .../external-modal/PopoutRendererClient.tsx | 84 ------ .../external-modal/PopoutRendererWeb.tsx | 76 ------ .../ui/react-elements/external-modal/index.ts | 8 +- .../internal-modal/Controller.ts | 112 -------- .../react-elements/internal-modal/Modal.scss | 249 ------------------ .../internal-modal/Renderer.tsx | 106 -------- .../js/ui/react-elements/modal/Controller.ts | 134 ++++++++++ .../js/ui/react-elements/modal/Definitions.ts | 80 ++++-- shared/js/ui/react-elements/modal/Registry.ts | 6 + .../js/ui/react-elements/modal/Renderer.scss | 223 ++++++++++++++++ .../js/ui/react-elements/modal/Renderer.tsx | 200 ++++++++++++-- shared/js/ui/react-elements/modal/TeaCup.png | Bin 0 -> 65631 bytes shared/js/ui/react-elements/modal/index.ts | 36 +-- .../react-elements/modal/internal/index.tsx | 172 ++++++++++++ shared/js/ui/tree/EntryTags.scss | 2 +- shared/js/ui/tree/EntryTags.tsx | 52 +++- web/app/legacy/audio-lib/index.ts | 2 +- 43 files changed, 1371 insertions(+), 831 deletions(-) create mode 100644 shared/js/ui/react-elements/external-modal/ModalRenderer.scss create mode 100644 shared/js/ui/react-elements/external-modal/ModalRenderer.tsx delete mode 100644 shared/js/ui/react-elements/external-modal/PopoutRenderer.scss delete mode 100644 shared/js/ui/react-elements/external-modal/PopoutRendererClient.tsx delete mode 100644 shared/js/ui/react-elements/external-modal/PopoutRendererWeb.tsx delete mode 100644 shared/js/ui/react-elements/internal-modal/Controller.ts delete mode 100644 shared/js/ui/react-elements/internal-modal/Modal.scss delete mode 100644 shared/js/ui/react-elements/internal-modal/Renderer.tsx create mode 100644 shared/js/ui/react-elements/modal/Controller.ts create mode 100644 shared/js/ui/react-elements/modal/Renderer.scss create mode 100644 shared/js/ui/react-elements/modal/TeaCup.png create mode 100644 shared/js/ui/react-elements/modal/internal/index.tsx diff --git a/shared/js/tree/EntryTagsHandler.ts b/shared/js/tree/EntryTagsHandler.ts index c66fa09d..df222734 100644 --- a/shared/js/tree/EntryTagsHandler.ts +++ b/shared/js/tree/EntryTagsHandler.ts @@ -88,6 +88,31 @@ function handleIpcMessage(type: string, payload: any) { channel?.showContextMenu(pageX, pageY); break; } + case "contextmenu-server": { + const { + handlerId, + serverUniqueId, + + pageX, + pageY + } = payload; + + if(typeof pageX !== "number" || typeof pageY !== "number") { + logWarn(LogCategory.IPC, tr("Received server context menu action with an invalid page coordinated: %ox%o."), pageX, pageY); + return; + } + + if(typeof handlerId !== "string") { + logWarn(LogCategory.IPC, tr("Received server context menu action with an invalid handler id: %o."), handlerId); + return; + } + + const handler = server_connections.findConnection(handlerId); + if(!handler?.connected || (serverUniqueId && handler.getCurrentServerUniqueId() !== serverUniqueId)) { + return; + } + handler.channelTree.server.showContextMenu(pageX, pageY); + } } } diff --git a/shared/js/tree/Server.ts b/shared/js/tree/Server.ts index bc581133..9d3159ea 100644 --- a/shared/js/tree/Server.ts +++ b/shared/js/tree/Server.ts @@ -147,6 +147,18 @@ export function parseServerAddress(address: string) : ServerAddress | undefined } } +export function stringifyServerAddress(address: ServerAddress) : string { + let result = address.host; + if(address.port !== 9987) { + if(address.host.indexOf(":") === -1) { + result += ":" + address.port; + } else { + result = "[" + result + "]:" + address.port; + } + } + return result; +} + export interface ServerEvents extends ChannelTreeEntryEvents { notify_properties_updated: { updated_properties: Partial; diff --git a/shared/js/ui/modal/ModalChangeVolumeNew.tsx b/shared/js/ui/modal/ModalChangeVolumeNew.tsx index da04b91c..98235d7d 100644 --- a/shared/js/ui/modal/ModalChangeVolumeNew.tsx +++ b/shared/js/ui/modal/ModalChangeVolumeNew.tsx @@ -5,7 +5,7 @@ import {Button} from "tc-shared/ui/react-elements/Button"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events"; import {ClientEntry, MusicClientEntry} from "tc-shared/tree/Client"; -import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller"; +import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions"; const cssStyle = require("./ModalChangeVolume.scss"); diff --git a/shared/js/ui/modal/ModalGroupCreate.tsx b/shared/js/ui/modal/ModalGroupCreate.tsx index 01089892..ed08021f 100644 --- a/shared/js/ui/modal/ModalGroupCreate.tsx +++ b/shared/js/ui/modal/ModalGroupCreate.tsx @@ -11,9 +11,9 @@ import PermissionType from "tc-shared/permission/PermissionType"; import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal"; import {tra} from "tc-shared/i18n/localize"; -import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller"; import {ErrorCode} from "tc-shared/connection/ErrorCode"; import {LogCategory, logError} from "tc-shared/log"; +import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions"; const cssStyle = require("./ModalGroupCreate.scss"); diff --git a/shared/js/ui/modal/ModalGroupPermissionCopy.tsx b/shared/js/ui/modal/ModalGroupPermissionCopy.tsx index a18f3500..4dc637a0 100644 --- a/shared/js/ui/modal/ModalGroupPermissionCopy.tsx +++ b/shared/js/ui/modal/ModalGroupPermissionCopy.tsx @@ -11,9 +11,9 @@ import PermissionType from "tc-shared/permission/PermissionType"; import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal"; import {tra} from "tc-shared/i18n/localize"; -import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller"; import {ErrorCode} from "tc-shared/connection/ErrorCode"; import {LogCategory, logWarn} from "tc-shared/log"; +import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions"; const cssStyle = require("./ModalGroupPermissionCopy.scss"); diff --git a/shared/js/ui/modal/bookmarks-add-server/Controller.ts b/shared/js/ui/modal/bookmarks-add-server/Controller.ts index 623d2d06..ac091a59 100644 --- a/shared/js/ui/modal/bookmarks-add-server/Controller.ts +++ b/shared/js/ui/modal/bookmarks-add-server/Controller.ts @@ -1,5 +1,134 @@ import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import { + ModalBookmarksAddServerEvents, + ModalBookmarksAddServerVariables, TargetBookmarkInfo +} from "tc-shared/ui/modal/bookmarks-add-server/Definitions"; +import {Registry} from "tc-events"; +import {createIpcUiVariableProvider, IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable"; +import {spawnModal} from "tc-shared/ui/react-elements/modal"; +import {stringifyServerAddress} from "tc-shared/tree/Server"; +import {bookmarks} from "tc-shared/Bookmarks"; + +class Controller { + readonly handler: ConnectionHandler; + readonly variables: IpcUiVariableProvider; + readonly events: Registry; + + private readonly serverInfo: TargetBookmarkInfo; + + private readonly targetChannelId: number; + private readonly targetChannelPassword: string; + + private readonly targetServerAddress: string; + private readonly targetServerPassword: string; + + private readonly connectProfile: string; + + private bookmarkName: string; + private useCurrentChannel: boolean; + + constructor(handler: ConnectionHandler) { + this.handler = handler; + + this.variables = createIpcUiVariableProvider(); + this.events = new Registry(); + + if(handler.connected && handler.getClient().currentChannel()) { + const currentChannel = handler.getClient().currentChannel(); + const handshakeHandler = handler.serverConnection.handshake_handler(); + + this.targetServerAddress = stringifyServerAddress(handler.channelTree.server.remote_address); + /* Password will be hashed since we're already connected to the server */ + this.targetServerPassword = handshakeHandler.parameters.serverPassword; + this.connectProfile = handshakeHandler.parameters.profile.id; + + this.targetChannelId = currentChannel.channelId; + this.targetChannelPassword = currentChannel.getCachedPasswordHash(); + + this.serverInfo = { + type: "success", + + handlerId: handler.handlerId, + + serverName: handler.channelTree.server.properties.virtualserver_name, + serverUniqueId: handler.getCurrentServerUniqueId(), + + currentChannelName: currentChannel.channelName(), + currentChannelId: currentChannel.channelId + }; + + this.bookmarkName = this.serverInfo.serverName; + } else { + this.serverInfo = { type: "not-connected" }; + } + + this.useCurrentChannel = false; + + this.variables.setVariableProvider("serverInfo", () => this.serverInfo); + this.variables.setVariableProvider("bookmarkNameValid", () => { + if(!this.bookmarkName) { + return false; + } + + return this.bookmarkName.length > 0 && this.bookmarkName.length < 40; + }); + + this.variables.setVariableProvider("bookmarkName", () => this.bookmarkName); + this.variables.setVariableEditor("bookmarkName", newValue => { + this.bookmarkName = newValue; + this.variables.sendVariable("bookmarkNameValid"); + }); + + this.variables.setVariableProvider("saveCurrentChannel", () => this.useCurrentChannel); + this.variables.setVariableEditor("saveCurrentChannel", newValue => { + this.useCurrentChannel = newValue; + }); + + this.events.on("action_add_bookmark", () => { + if(this.serverInfo.type !== "success") { + return; + } + + if(!this.variables.getVariableSync("bookmarkNameValid")) { + return; + } + + bookmarks.createBookmark({ + displayName: this.bookmarkName || this.serverInfo.serverName, + connectProfile: this.connectProfile, + connectOnStartup: false, + + defaultChannelPasswordHash: this.targetChannelPassword, + defaultChannel: this.useCurrentChannel ? ("/" + this.targetChannelId) : undefined, + + serverPasswordHash: this.targetServerPassword, + serverAddress: this.targetServerAddress, + + previousEntry: undefined, + parentEntry: undefined + }); + + this.events.fire("notify_bookmark_added"); + }); + } + + destroy() { + this.events.destroy(); + this.variables.destroy(); + } +} export function spawnModalAddCurrentServerToBookmarks(handler: ConnectionHandler) { - /* TODO! */ + const controller = new Controller(handler); + const modal = spawnModal("modal-bookmark-add-server", [ + controller.events.generateIpcDescription(), + controller.variables.generateConsumerDescription() + ], { + popoutable: true, + popedOut: false + }); + + controller.events.on(["action_cancel", "notify_bookmark_added"], () => modal.destroy()); + modal.getEvents().on("destroy", () => controller.destroy()); + modal.show().then(undefined); } \ No newline at end of file diff --git a/shared/js/ui/modal/bookmarks-add-server/Definitions.ts b/shared/js/ui/modal/bookmarks-add-server/Definitions.ts index c71a40ae..4cd0c4ce 100644 --- a/shared/js/ui/modal/bookmarks-add-server/Definitions.ts +++ b/shared/js/ui/modal/bookmarks-add-server/Definitions.ts @@ -1,7 +1,28 @@ -export interface ModalBookmarksAddServerVariables { +export type TargetBookmarkInfo = { + type: "success", + handlerId: string, + serverName: string, + serverUniqueId: string, + + currentChannelId: number, + currentChannelName: string, +} | { + type: "not-connected" | "loading" +}; + +export interface ModalBookmarksAddServerVariables { + readonly serverInfo: TargetBookmarkInfo, + + bookmarkName: string, + bookmarkNameValid: boolean, + + saveCurrentChannel: boolean, } export interface ModalBookmarksAddServerEvents { + action_add_bookmark: {}, + action_cancel: {}, + notify_bookmark_added: {} } \ No newline at end of file diff --git a/shared/js/ui/modal/bookmarks-add-server/Renderer.scss b/shared/js/ui/modal/bookmarks-add-server/Renderer.scss index e69de29b..b18ce053 100644 --- a/shared/js/ui/modal/bookmarks-add-server/Renderer.scss +++ b/shared/js/ui/modal/bookmarks-add-server/Renderer.scss @@ -0,0 +1,61 @@ +.container { + display: flex; + flex-direction: column; + justify-content: stretch; + + padding: .5em; + height: 100%; +} + +.bookmarkInfo { + display: flex; + flex-direction: column; + justify-content: flex-start; + + margin-bottom: 1em; + + .text { + &.addServer {} + + &.serverName { + + } + + &.bookmarkName { + margin-top: .5em; + color: #557edc; + } + } + + .editableBookmarkName { + margin-bottom: .5em; + } + + .channelIcon { + width: 1em; + height: 1em; + } + + .channelTooltipOuter { + cursor: pointer; + display: flex; + flex-direction: column; + justify-content: center; + + margin-left: .5em; + align-self: center; + } + + .channelLabel { + display: flex; + flex-direction: row; + } +} + +.buttons { + display: flex; + flex-direction: row; + justify-content: space-between; + + margin-top: auto; +} \ No newline at end of file diff --git a/shared/js/ui/modal/bookmarks-add-server/Renderer.tsx b/shared/js/ui/modal/bookmarks-add-server/Renderer.tsx index 21d7c152..b8f7e882 100644 --- a/shared/js/ui/modal/bookmarks-add-server/Renderer.tsx +++ b/shared/js/ui/modal/bookmarks-add-server/Renderer.tsx @@ -1,13 +1,164 @@ import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions"; -import React from "react"; +import React, {useContext, useEffect, useRef} from "react"; +import {UiVariableConsumer} from "tc-shared/ui/utils/Variable"; +import {IpcRegistryDescription, Registry} from "tc-events"; +import { + ModalBookmarksAddServerEvents, + ModalBookmarksAddServerVariables, TargetBookmarkInfo +} from "tc-shared/ui/modal/bookmarks-add-server/Definitions"; +import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable"; +import {Translatable} from "tc-shared/ui/react-elements/i18n"; +import {Button} from "tc-shared/ui/react-elements/Button"; +import {ControlledBoxedInputField} from "tc-shared/ui/react-elements/InputField"; +import {Checkbox} from "tc-shared/ui/react-elements/Checkbox"; +import {ChannelTag, ServerTag} from "tc-shared/ui/tree/EntryTags"; +import {IconTooltip} from "tc-shared/ui/react-elements/Tooltip"; + +const cssStyle = require("./Renderer.scss"); + +const EventContext = React.createContext>(undefined); +const VariablesContext = React.createContext>(undefined); +const BookmarkInfoContext = React.createContext(undefined); + +const BookmarkInfoProvider = React.memo((props: { children }) => { + const variables = useContext(VariablesContext); + const info = variables.useReadOnly("serverInfo", undefined, { type: "loading" }); + return ( + + {props.children} + + ); +}); + +const BookmarkName = React.memo(() => { + const events = useContext(EventContext); + const variables = useContext(VariablesContext); + const name = variables.useVariable("bookmarkName", undefined); + const nameValid = variables.useReadOnly("bookmarkNameValid", undefined, true); + const info = useContext(BookmarkInfoContext); + + const refField = useRef(); + useEffect(() => { + if(info.type === "success") { + refField.current?.focus(); + } + }, [ info.type === "success" ]); + + return ( + name.setValue(newValue, true)} + onBlur={() => name.setValue(name.localValue)} + onEnter={() => events.fire("action_add_bookmark")} + + disabled={info.type !== "success"} + finishOnEnter={true} + /> + ); +}); + +const BookmarkChannel = React.memo(() => { + const variables = useContext(VariablesContext); + const info = useContext(BookmarkInfoContext); + const saveCurrentChannel = variables.useVariable("saveCurrentChannel", undefined, false); + + let helpIcon; + if(info.type === "success") { + helpIcon = ( + + Current channel:
+ +
+ ) + } + + return ( + saveCurrentChannel.setValue(value)} + label={
Save current channel as default channel {helpIcon}
} + /> + ); +}); + +const ServerName = React.memo(() => { + const info = useContext(BookmarkInfoContext); + if(info.type !== "success") { + return null; + } + + return ; +}); + +const RendererBookmarkInfo = React.memo(() => { + return ( +
+
Add server to bookmarks:
+
+
Bookmark name:
+ + +
+ ) +}); + +const RendererButtons = React.memo(() => { + const info = useContext(BookmarkInfoContext); + const events = useContext(EventContext); + const variables = useContext(VariablesContext); + const nameValid = variables.useReadOnly("bookmarkNameValid", undefined, true); + + return ( +
+ + +
+ ) +}) class ModalBookmarksAddServer extends AbstractModal { + private readonly variables: UiVariableConsumer; + private readonly events: Registry; + + constructor(events: IpcRegistryDescription, variables: IpcVariableDescriptor) { + super(); + + this.variables = createIpcUiVariableConsumer(variables); + this.events = Registry.fromIpcDescription(events); + } + renderBody(): React.ReactElement { - return undefined; + return ( + + +
+ + + + +
+
+
+ ); } renderTitle(): string | React.ReactElement { - return undefined; + return Add server to bookmarks; } +} -} \ No newline at end of file +export = ModalBookmarksAddServer; \ No newline at end of file diff --git a/shared/js/ui/modal/bookmarks/Renderer.tsx b/shared/js/ui/modal/bookmarks/Renderer.tsx index 8e752e5c..7e9fba56 100644 --- a/shared/js/ui/modal/bookmarks/Renderer.tsx +++ b/shared/js/ui/modal/bookmarks/Renderer.tsx @@ -579,6 +579,7 @@ const BookmarkSettingChannelPassword = () => { } const BookmarkInfoRenderer = React.memo(() => { + const selectedBookmark = useContext(SelectedBookmarkIdContext); const bookmarkInfo = useContext(SelectedBookmarkInfoContext); let connectCount = bookmarkInfo ? Math.max(bookmarkInfo.connectCountUniqueId, bookmarkInfo.connectCountAddress) : -1; @@ -635,6 +636,11 @@ const BookmarkInfoRenderer = React.memo(() => { You never connected to that server. +
+
+ {/* bookmark is a directory */} +
+
); }); @@ -774,11 +780,11 @@ class ModalBookmarks extends AbstractModal { this.variables.destroy(); } - renderBody(renderBody): React.ReactElement { + renderBody(): React.ReactElement { return ( -
+
diff --git a/shared/js/ui/modal/channel-edit/Renderer.tsx b/shared/js/ui/modal/channel-edit/Renderer.tsx index 761316d1..a22da924 100644 --- a/shared/js/ui/modal/channel-edit/Renderer.tsx +++ b/shared/js/ui/modal/channel-edit/Renderer.tsx @@ -23,7 +23,7 @@ import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon"; import {getIconManager} from "tc-shared/file/Icons"; import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions"; -import {ChannelNameAlignment, ChannelNameParser} from "tc-shared/tree/Channel"; +import {ChannelNameAlignment} from "tc-shared/tree/Channel"; const cssStyle = require("./Renderer.scss"); diff --git a/shared/js/ui/modal/connect/Renderer.tsx b/shared/js/ui/modal/connect/Renderer.tsx index be6de880..d4dd7fad 100644 --- a/shared/js/ui/modal/connect/Renderer.tsx +++ b/shared/js/ui/modal/connect/Renderer.tsx @@ -11,7 +11,6 @@ import {Button} from "tc-shared/ui/react-elements/Button"; import {ControlledFlatInputField, ControlledSelect, FlatInputField} from "tc-shared/ui/react-elements/InputField"; import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons"; import {ClientIcon} from "svg-sprites/client-icons"; -import * as i18n from "../../../i18n/country"; import {getIconManager} from "tc-shared/file/Icons"; import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon"; import {UiVariableConsumer} from "tc-shared/ui/utils/Variable"; @@ -414,10 +413,6 @@ class ConnectModal extends AbstractModal { return Connect to a server; } - color(): "none" | "blue" { - return "blue"; - } - verticalAlignment(): "top" | "center" | "bottom" { return "top"; } diff --git a/shared/js/ui/modal/invite/Renderer.tsx b/shared/js/ui/modal/invite/Renderer.tsx index a84bcd54..d5d4ff03 100644 --- a/shared/js/ui/modal/invite/Renderer.tsx +++ b/shared/js/ui/modal/invite/Renderer.tsx @@ -404,13 +404,4 @@ class ModalInvite extends AbstractModal { return <>Invite People to {this.serverName}; } } -export = ModalInvite; - -/* -const modal = spawnModal("global-settings-editor", [ events.generateIpcDescription() ], { popoutable: true, popedOut: false }); -modal.show(); -modal.getEvents().on("destroy", () => { - events.fire("notify_destroy"); - events.destroy(); -}); - */ \ No newline at end of file +export = ModalInvite; \ No newline at end of file diff --git a/shared/js/ui/modal/permission/ModalPermissionEditor.tsx b/shared/js/ui/modal/permission/ModalPermissionEditor.tsx index 935874da..37e5cb07 100644 --- a/shared/js/ui/modal/permission/ModalPermissionEditor.tsx +++ b/shared/js/ui/modal/permission/ModalPermissionEditor.tsx @@ -30,11 +30,11 @@ import { } from "tc-shared/ui/modal/permission/SenselessPermissions"; import {spawnGroupCreate} from "tc-shared/ui/modal/ModalGroupCreate"; import {spawnModalGroupPermissionCopy} from "tc-shared/ui/modal/ModalGroupPermissionCopy"; -import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller"; import {ErrorCode} from "tc-shared/connection/ErrorCode"; import {PermissionEditorTab} from "tc-shared/events/GlobalEvents"; import {LogCategory, logError, logWarn} from "tc-shared/log"; import {useTr} from "tc-shared/ui/react-elements/Helper"; +import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions"; const cssStyle = require("./ModalPermissionEditor.scss"); diff --git a/shared/js/ui/modal/transfer/FileBrowserControllerRemote.ts b/shared/js/ui/modal/transfer/FileBrowserControllerRemote.ts index a3739daa..c6bb8e0c 100644 --- a/shared/js/ui/modal/transfer/FileBrowserControllerRemote.ts +++ b/shared/js/ui/modal/transfer/FileBrowserControllerRemote.ts @@ -1,5 +1,5 @@ import {ConnectionHandler} from "../../../ConnectionHandler"; -import {Registry} from "../../../events"; +import {Registry} from "tc-events"; import {FileType} from "../../../file/FileManager"; import {CommandResult} from "../../../connection/ServerConnectionDeclaration"; import PermissionType from "../../../permission/PermissionType"; diff --git a/shared/js/ui/modal/transfer/FileTransferInfoController.ts b/shared/js/ui/modal/transfer/FileTransferInfoController.ts index 5d88ff4a..b702b4f3 100644 --- a/shared/js/ui/modal/transfer/FileTransferInfoController.ts +++ b/shared/js/ui/modal/transfer/FileTransferInfoController.ts @@ -1,5 +1,5 @@ import {ConnectionHandler} from "../../../ConnectionHandler"; -import {Registry} from "../../../events"; +import {Registry} from "tc-events"; import { FileTransfer, FileTransferDirection, diff --git a/shared/js/ui/modal/transfer/ModalFileTransfer.tsx b/shared/js/ui/modal/transfer/ModalFileTransfer.tsx index da7f588b..51827845 100644 --- a/shared/js/ui/modal/transfer/ModalFileTransfer.tsx +++ b/shared/js/ui/modal/transfer/ModalFileTransfer.tsx @@ -6,10 +6,10 @@ import {FileTransferInfo} from "./FileTransferInfo"; import {initializeRemoteFileBrowserController} from "./FileBrowserControllerRemote"; import {initializeTransferInfoController} from "./FileTransferInfoController"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; -import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller"; import {server_connections} from "tc-shared/ConnectionManager"; import {channelPathPrefix, FileBrowserEvents} from "tc-shared/ui/modal/transfer/FileDefinitions"; import {TransferInfoEvents} from "tc-shared/ui/modal/transfer/FileTransferInfoDefinitions"; +import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions"; const cssStyle = require("./FileBrowserRenderer.scss"); diff --git a/shared/js/ui/modal/video-source/Renderer.tsx b/shared/js/ui/modal/video-source/Renderer.tsx index 17134c89..bb1d0ce3 100644 --- a/shared/js/ui/modal/video-source/Renderer.tsx +++ b/shared/js/ui/modal/video-source/Renderer.tsx @@ -10,7 +10,6 @@ import { VideoPreviewStatus, VideoSourceState } from "tc-shared/ui/modal/video-source/Definitions"; -import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller"; import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n"; import {BoxedInputField, Select} from "tc-shared/ui/react-elements/InputField"; import {Button} from "tc-shared/ui/react-elements/Button"; @@ -20,6 +19,7 @@ import {Checkbox} from "tc-shared/ui/react-elements/Checkbox"; import {Tab, TabEntry} from "tc-shared/ui/react-elements/Tab"; import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; import {ScreenCaptureDevice} from "tc-shared/video/VideoSource"; +import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions"; const cssStyle = require("./Renderer.scss"); const ModalEvents = React.createContext>(undefined); diff --git a/shared/js/ui/modal/whats-new/Controller.tsx b/shared/js/ui/modal/whats-new/Controller.tsx index bca43401..24b2dcda 100644 --- a/shared/js/ui/modal/whats-new/Controller.tsx +++ b/shared/js/ui/modal/whats-new/Controller.tsx @@ -1,9 +1,9 @@ import {spawnReactModal} from "tc-shared/ui/react-elements/modal"; -import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller"; import * as React from "react"; import {WhatsNew} from "tc-shared/ui/modal/whats-new/Renderer"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {ChangeLog} from "tc-shared/update/ChangeLog"; +import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions"; export function spawnUpdatedModal(changes: { changesUI?: ChangeLog, changesClient?: ChangeLog }) { const modal = spawnReactModal(class extends InternalModal { diff --git a/shared/js/ui/react-elements/InputField.tsx b/shared/js/ui/react-elements/InputField.tsx index ae0c3465..6d4b0b8d 100644 --- a/shared/js/ui/react-elements/InputField.tsx +++ b/shared/js/ui/react-elements/InputField.tsx @@ -34,6 +34,7 @@ export const ControlledBoxedInputField = (props: { onBlur?: () => void, finishOnEnter?: boolean, + refInput?: React.RefObject }) => { return ( @@ -61,6 +62,8 @@ export const ControlledBoxedInputField = (props: { ReactElement | ReactElement[] | string; + className?: string; } export class Tooltip extends React.Component { @@ -103,6 +104,7 @@ export class Tooltip extends React.Component { onMouseLeave={() => this.setState({ hovered: false })} onClick={() => this.setState({ hovered: !this.state.hovered })} style={{ cursor: "pointer" }} + className={this.props.className} > {this.props.children} @@ -124,8 +126,9 @@ export class Tooltip extends React.Component { private onMouseEnter(event: React.MouseEvent) { /* check if may only the span has been hovered, should not be the case! */ - if(event.target === this.refContainer.current) + if(event.target === this.refContainer.current) { return; + } this.setState({ hovered: true }); @@ -149,8 +152,8 @@ export class Tooltip extends React.Component { } } -export const IconTooltip = (props: { children?: React.ReactElement | React.ReactElement[], className?: string }) => ( - props.children}> +export const IconTooltip = (props: { children?: React.ReactElement | React.ReactElement[], className?: string, outerClassName?: string }) => ( + props.children} className={props.outerClassName}>
{""}
diff --git a/shared/js/ui/react-elements/external-modal/Controller.ts b/shared/js/ui/react-elements/external-modal/Controller.ts index 57e58af2..a2e42559 100644 --- a/shared/js/ui/react-elements/external-modal/Controller.ts +++ b/shared/js/ui/react-elements/external-modal/Controller.ts @@ -8,14 +8,15 @@ import { Popout2ControllerMessages, PopoutIPCMessage } from "../../../ui/react-elements/external-modal/IPCMessage"; -import {ModalController, ModalEvents, ModalOptions, ModalState} from "../../../ui/react-elements/ModalDefinitions"; +import {ModalEvents, ModalOptions, ModalState} from "../../../ui/react-elements/ModalDefinitions"; import {guid} from "tc-shared/crypto/uid"; +import {ModalInstanceController, ModalInstanceEvents} from "tc-shared/ui/react-elements/modal/Definitions"; -export abstract class AbstractExternalModalController extends EventControllerBase<"controller"> implements ModalController { +export abstract class AbstractExternalModalController extends EventControllerBase<"controller"> implements ModalInstanceController { public readonly modalType: string; public readonly constructorArguments: any[]; - private readonly modalEvents: Registry; + private readonly modalEvents: Registry; private modalState: ModalState = ModalState.DESTROYED; private readonly documentUnloadListener: () => void; @@ -26,7 +27,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas this.modalType = modalType; this.constructorArguments = constructorArguments; - this.modalEvents = new Registry(); + this.modalEvents = new Registry(); this.ipcChannel = ipc.getIpcInstance().createChannel(kPopoutIPCChannelId); this.ipcChannel.messageHandler = this.handleIPCMessage.bind(this); @@ -38,7 +39,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas return {}; /* FIXME! */ } - getEvents(): Registry { + getEvents(): Registry { return this.modalEvents; } @@ -85,7 +86,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas } window.addEventListener("unload", this.documentUnloadListener); - this.modalEvents.fire("open"); + this.modalEvents.fire("notify_open"); } private doDestroyWindow() { @@ -99,12 +100,13 @@ export abstract class AbstractExternalModalController extends EventControllerBas this.doDestroyWindow(); this.modalState = ModalState.HIDDEN; - this.modalEvents.fire("close"); + this.modalEvents.fire("notify_close"); } destroy() { - if(this.modalState === ModalState.DESTROYED) + if(this.modalState === ModalState.DESTROYED) { return; + } this.doDestroyWindow(); if(this.ipcChannel) { @@ -113,7 +115,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas this.destroyIPC(); this.modalState = ModalState.DESTROYED; - this.modalEvents.fire("destroy"); + this.modalEvents.fire("notify_destroy"); } protected handleWindowClosed() { diff --git a/shared/js/ui/react-elements/external-modal/ModalRenderer.scss b/shared/js/ui/react-elements/external-modal/ModalRenderer.scss new file mode 100644 index 00000000..f1201346 --- /dev/null +++ b/shared/js/ui/react-elements/external-modal/ModalRenderer.scss @@ -0,0 +1,20 @@ +@import "../../../../css/static/mixin"; + +/* FIXME: Remove this wired import */ +:global { + @import "../../../../css/static/general"; + @import "../../../../css/static/modal"; +} + +html, body { + margin: 0; + padding: 0; + + height: 100vh; + width: 100vw; + + background: #19191b; + color: #999; + + overflow: hidden; +} diff --git a/shared/js/ui/react-elements/external-modal/ModalRenderer.tsx b/shared/js/ui/react-elements/external-modal/ModalRenderer.tsx new file mode 100644 index 00000000..68e776cd --- /dev/null +++ b/shared/js/ui/react-elements/external-modal/ModalRenderer.tsx @@ -0,0 +1,64 @@ +import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions"; +import * as ReactDOM from "react-dom"; +import * as React from "react"; +import { + ModalBodyRenderer, ModalFrameRenderer, + ModalFrameTopRenderer, + WindowModalRenderer +} from "tc-shared/ui/react-elements/modal/Renderer"; + +import "./ModalRenderer.scss"; + +export interface ModalControlFunctions { + close(); + minimize(); +} + +export class ModalRenderer { + private readonly functionController: ModalControlFunctions; + private readonly container: HTMLDivElement; + + constructor(functionController: ModalControlFunctions) { + this.functionController = functionController; + + this.container = document.createElement("div"); + document.body.appendChild(this.container); + } + + renderModal(modal: AbstractModal | undefined) { + if(typeof modal === "undefined") { + ReactDOM.unmountComponentAtNode(this.container); + return; + } + + if(__build.target === "client") { + ReactDOM.render( + + this.functionController.close()} + onMinimize={() => this.functionController.minimize()} + /> + + , + this.container + ); + } else { + ReactDOM.render( + + this.functionController.close()} + onMinimize={() => this.functionController.minimize()} + /> + + , + this.container + ); + } + } +} \ No newline at end of file diff --git a/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts b/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts index c05b9708..2d7ccdbb 100644 --- a/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts +++ b/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts @@ -2,16 +2,16 @@ import * as loader from "tc-loader"; import {Stage} from "tc-loader"; import * as ipc from "../../../ipc/BrowserIPC"; import * as i18n from "../../../i18n/localize"; -import {AbstractModal, ModalRenderer} from "../../../ui/react-elements/ModalDefinitions"; +import {AbstractModal} from "../../../ui/react-elements/ModalDefinitions"; import {AppParameters} from "../../../settings"; import {getPopoutController} from "./PopoutController"; -import {WebModalRenderer} from "../../../ui/react-elements/external-modal/PopoutRendererWeb"; -import {ClientModalRenderer} from "../../../ui/react-elements/external-modal/PopoutRendererClient"; import {setupJSRender} from "../../../ui/jsrender"; import "../../../file/RemoteAvatars"; import "../../../file/RemoteIcons"; import {findRegisteredModal} from "tc-shared/ui/react-elements/modal/Registry"; +import {ModalRenderer} from "tc-shared/ui/react-elements/external-modal/ModalRenderer"; +import {constructAbstractModalClass} from "tc-shared/ui/react-elements/modal/Definitions"; if("__native_client_init_shared" in window) { (window as any).__native_client_init_shared(__webpack_require__); @@ -47,18 +47,14 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { name: "modal renderer loader", priority: 10, function: async () => { - if(__build.target === "web") { - modalRenderer = new WebModalRenderer(); - } else { - modalRenderer = new ClientModalRenderer({ - close() { - getPopoutController().doClose() - }, - minimize() { - getPopoutController().doMinimize() - } - }); - } + modalRenderer = new ModalRenderer({ + close() { + getPopoutController().doClose() + }, + minimize() { + getPopoutController().doMinimize() + } + }); } }); @@ -88,7 +84,7 @@ loader.register_task(Stage.LOADED, { priority: 100, function: async () => { try { - modalInstance = new modalClass(...getPopoutController().getConstructorArguments()); + modalInstance = constructAbstractModalClass(modalClass, { windowed: true }, getPopoutController().getConstructorArguments()); modalRenderer.renderModal(modalInstance); } catch(error) { loader.critical_error("Failed to invoker modal", "Lookup the console for more detail"); diff --git a/shared/js/ui/react-elements/external-modal/PopoutRenderer.scss b/shared/js/ui/react-elements/external-modal/PopoutRenderer.scss deleted file mode 100644 index 6cb15278..00000000 --- a/shared/js/ui/react-elements/external-modal/PopoutRenderer.scss +++ /dev/null @@ -1,57 +0,0 @@ -@import "../../../../css/static/mixin"; - -/* FIXME: Remove this wired import */ -:global { - @import "../../../../css/static/general"; - @import "../../../../css/static/modal"; -} - -html, body { - margin: 0; - padding: 0; - - height: 100vh; - width: 100vw; - - background: #19191b; - color: #999; -} - -.container { - position: absolute; - - top: 0; - left: 0; - right: 0; - bottom: 0; - - display: flex; - flex-direction: column; - justify-content: stretch; - - max-height: 100vh; - max-width: 100vw; - - overflow: hidden; -} - -.container { - margin: 0!important; - - height: 100% !important; - width: 100% !important; - - .header { - -webkit-user-select: none; - -webkit-app-region: drag; - - .title { - --stay-alive: none; - } - } - - .body { - overflow: auto; - @include chat-scrollbar(); - } -} diff --git a/shared/js/ui/react-elements/external-modal/PopoutRendererClient.tsx b/shared/js/ui/react-elements/external-modal/PopoutRendererClient.tsx deleted file mode 100644 index 5d81e730..00000000 --- a/shared/js/ui/react-elements/external-modal/PopoutRendererClient.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import {InternalModalContentRenderer} from "tc-shared/ui/react-elements/internal-modal/Renderer"; -import {AbstractModal, ModalRenderer} from "tc-shared/ui/react-elements/ModalDefinitions"; -import * as ReactDOM from "react-dom"; -import * as React from "react"; - -export interface ModalControlFunctions { - close(); - minimize(); -} - -const cssStyle = require("./PopoutRenderer.scss"); -export class ClientModalRenderer implements ModalRenderer { - private readonly functionController: ModalControlFunctions; - - private readonly titleElement: HTMLTitleElement; - private readonly container: HTMLDivElement; - private readonly titleChangeObserver: MutationObserver; - - private titleContainer: HTMLDivElement; - private currentModal: AbstractModal; - - constructor(functionController: ModalControlFunctions) { - this.functionController = functionController; - - this.container = document.createElement("div"); - this.container.classList.add(cssStyle.container); - - const titleElements = document.getElementsByTagName("title"); - if(titleElements.length === 0) { - this.titleElement = document.createElement("title"); - document.head.appendChild(this.titleElement); - } else { - this.titleElement = titleElements[0]; - } - - document.body.append(this.container); - - this.titleChangeObserver = new MutationObserver(() => this.updateTitle()); - } - - renderModal(modal: AbstractModal | undefined) { - if(this.currentModal === modal) { - return; - } - - this.titleChangeObserver.disconnect(); - ReactDOM.unmountComponentAtNode(this.container); - - this.currentModal = modal; - ReactDOM.render( - this.functionController.close()} - onMinimize={() => this.functionController.minimize()} - - containerClass={cssStyle.container} - headerClass={cssStyle.header} - headerTitleClass={cssStyle.title} - bodyClass={cssStyle.body} - - windowed={true} - />, - this.container, - () => { - this.titleContainer = this.container.querySelector("." + cssStyle.title) as HTMLDivElement; - this.titleChangeObserver.observe(this.titleContainer, { - attributes: true, - subtree: true, - childList: true, - characterData: true - }); - this.updateTitle(); - } - ); - } - - private updateTitle() { - if(!this.titleContainer) { - return; - } - - this.titleElement.innerText = this.titleContainer.textContent; - } -} \ No newline at end of file diff --git a/shared/js/ui/react-elements/external-modal/PopoutRendererWeb.tsx b/shared/js/ui/react-elements/external-modal/PopoutRendererWeb.tsx deleted file mode 100644 index d2c7bd81..00000000 --- a/shared/js/ui/react-elements/external-modal/PopoutRendererWeb.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import {AbstractModal, ModalRenderer} from "tc-shared/ui/react-elements/ModalDefinitions"; - -const cssStyle = require("./PopoutRenderer.scss"); - -class TitleRenderer { - private readonly htmlContainer: HTMLElement; - private modalInstance: AbstractModal; - - constructor() { - const titleElements = document.getElementsByTagName("title"); - if(titleElements.length === 0) { - this.htmlContainer = document.createElement("title"); - document.head.appendChild(this.htmlContainer); - } else { - this.htmlContainer = titleElements[0]; - } - } - - setInstance(instance: AbstractModal) { - if(this.modalInstance) { - ReactDOM.unmountComponentAtNode(this.htmlContainer); - } - - this.modalInstance = instance; - if(this.modalInstance) { - ReactDOM.render(<>{this.modalInstance.renderTitle()}, this.htmlContainer); - } - } -} - -class BodyRenderer { - private readonly htmlContainer: HTMLElement; - private modalInstance: AbstractModal; - - constructor() { - this.htmlContainer = document.createElement("div"); - this.htmlContainer.classList.add(cssStyle.container); - - document.body.appendChild(this.htmlContainer); - } - - setInstance(instance: AbstractModal) { - if(this.modalInstance) { - ReactDOM.unmountComponentAtNode(this.htmlContainer); - } - - this.modalInstance = instance; - if(this.modalInstance) { - ReactDOM.render(<>{this.modalInstance.renderBody({ windowed: true })}, this.htmlContainer); - } - } -} - -export class WebModalRenderer implements ModalRenderer { - private readonly titleRenderer: TitleRenderer; - private readonly bodyRenderer: BodyRenderer; - - private currentModal: AbstractModal; - - constructor() { - this.titleRenderer = new TitleRenderer(); - this.bodyRenderer = new BodyRenderer(); - } - - renderModal(modal: AbstractModal | undefined) { - if(this.currentModal === modal) { - return; - } - - this.currentModal = modal; - this.titleRenderer.setInstance(modal); - this.bodyRenderer.setInstance(modal); - } -} \ No newline at end of file diff --git a/shared/js/ui/react-elements/external-modal/index.ts b/shared/js/ui/react-elements/external-modal/index.ts index 08f65911..99705f5d 100644 --- a/shared/js/ui/react-elements/external-modal/index.ts +++ b/shared/js/ui/react-elements/external-modal/index.ts @@ -1,14 +1,14 @@ +import {ModalInstanceController, ModalOptions} from "../modal/Definitions"; import "./Controller"; -import {ModalController, ModalOptions} from "../ModalDefinitions"; -export type ControllerFactory = (modalType: string, constructorArguments?: any[], options?: ModalOptions) => ModalController; +export type ControllerFactory = (modalType: string, constructorArguments?: any[], options?: ModalOptions) => ModalInstanceController; + let modalControllerFactory: ControllerFactory; - export function setExternalModalControllerFactory(factory: ControllerFactory) { modalControllerFactory = factory; } -export function spawnExternalModal(modalType: string, constructorArguments?: any[], options?: ModalOptions) : ModalController { +export function spawnExternalModal(modalType: string, constructorArguments: any[], options: ModalOptions) : ModalInstanceController { if(typeof modalControllerFactory === "undefined") { throw tr("No external modal factory has been set"); } diff --git a/shared/js/ui/react-elements/internal-modal/Controller.ts b/shared/js/ui/react-elements/internal-modal/Controller.ts deleted file mode 100644 index 206f5e21..00000000 --- a/shared/js/ui/react-elements/internal-modal/Controller.ts +++ /dev/null @@ -1,112 +0,0 @@ -import {Registry} from "../../../events"; -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import { - AbstractModal, - ModalController, - ModalEvents, - ModalOptions, - ModalState -} from "../../../ui/react-elements/ModalDefinitions"; -import {InternalModalRenderer} from "../../../ui/react-elements/internal-modal/Renderer"; -import {tr} from "tc-shared/i18n/localize"; -import {RegisteredModal} from "tc-shared/ui/react-elements/modal/Registry"; - -export class InternalModalController implements ModalController { - readonly events: Registry; - - private readonly modalType: RegisteredModal; - private readonly constructorArguments: any[]; - private modalInstance: AbstractModal; - - private initializedPromise: Promise; - - private domElement: Element; - private refModal: React.RefObject; - private modalState_: ModalState = ModalState.HIDDEN; - - constructor(modalType: RegisteredModal, constructorArguments: any[]) { - this.modalType = modalType; - this.constructorArguments = constructorArguments; - - this.events = new Registry(); - this.initializedPromise = this.initialize(); - } - - getOptions(): Readonly { - /* FIXME! */ - return {}; - } - - getEvents(): Registry { - return this.events; - } - - getState() { - return this.modalState_; - } - - private async initialize() { - this.refModal = React.createRef(); - this.domElement = document.createElement("div"); - - this.modalInstance = new (await this.modalType.classLoader()).default(...this.constructorArguments); - const element = React.createElement(InternalModalRenderer, { - ref: this.refModal, - modal: this.modalInstance, - onClose: () => this.destroy() - }); - document.body.appendChild(this.domElement); - await new Promise(resolve => { - ReactDOM.render(element, this.domElement, () => setTimeout(resolve, 0)); - }); - - this.modalInstance["onInitialize"](); - } - - async show() : Promise { - await this.initializedPromise; - if(this.modalState_ === ModalState.DESTROYED) { - throw tr("modal has been destroyed"); - } else if(this.modalState_ === ModalState.SHOWN) { - return; - } - - this.refModal.current?.setState({ show: true }); - this.modalState_ = ModalState.SHOWN; - this.modalInstance["onOpen"](); - this.events.fire("open"); - } - - async hide() : Promise { - await this.initializedPromise; - if(this.modalState_ === ModalState.DESTROYED) - throw tr("modal has been destroyed"); - else if(this.modalState_ === ModalState.HIDDEN) - return; - - this.refModal.current?.setState({ show: false }); - this.modalState_ = ModalState.HIDDEN; - this.modalInstance["onClose"](); - this.events.fire("close"); - - return new Promise(resolve => setTimeout(resolve, 500)); - } - - destroy() { - if(this.modalState_ === ModalState.SHOWN) { - this.hide().then(() => this.destroy()); - return; - } - - ReactDOM.unmountComponentAtNode(this.domElement); - this.domElement.remove(); - - this.domElement = undefined; - this.modalState_ = ModalState.DESTROYED; - this.modalInstance["onDestroy"](); - this.events.fire("destroy"); - } -} - -export abstract class InternalModal extends AbstractModal {} \ No newline at end of file diff --git a/shared/js/ui/react-elements/internal-modal/Modal.scss b/shared/js/ui/react-elements/internal-modal/Modal.scss deleted file mode 100644 index 0cccb4d4..00000000 --- a/shared/js/ui/react-elements/internal-modal/Modal.scss +++ /dev/null @@ -1,249 +0,0 @@ -@import "../../../../css/static/mixin"; -@import "../../../../css/static/properties"; - -html:root { - --modal-content-background: #19191b; -} - -.modal { - color: var(--text); /* base color */ - - overflow: auto; /* allow scrolling if a modal is too big */ - - background-color: rgba(0, 0, 0, 0.8); - - padding-right: 5%; - padding-left: 5%; - - z-index: 100000; - position: fixed; - - top: 0; - left: 0; - right: 0; - bottom: 0; - - display: flex; - flex-direction: column; - - opacity: 0; - margin-top: -1000vh; - - $animation_length: .3s; - @include transition(opacity $animation_length ease-in, margin-top $animation_length ease-in); - &.shown { - margin-top: 0; - opacity: 1; - } - - &.align-top { - justify-content: flex-start; - } - - &.align-center { - justify-content: center; - } - - &.align-bottom { - justify-content: flex-end; - } - - .dialog { - display: block; - - margin: 1.75rem 0; - - /* width calculations */ - align-items: center; - - /* height stuff */ - max-height: calc(100% - 3.5em); - } -} - -/* special modal types */ -.modal { - &.header-error .header { - background-color: #800000; - } - - &.header-info .header { - background-color: #014565; - } - - &.header-warning .header, - &.header-info .header, - &.header-error .header { - border-top-left-radius: .125rem; - border-top-right-radius: .125rem; - } -} - -.modal { - .dialog { - display: flex; - flex-direction: column; - justify-content: stretch; - - &.modal-dialog-centered { - justify-content: stretch; - } - } -} - -/* special general modals */ -.modal { - /* TODO! */ - .modal-body.modal-blue { - border-left: 2px solid #0a73d2; - } - .modal-body.modal-green { - border-left: 2px solid #00d400; - } - .modal-body.modal-red { - border: none; - border-left: 2px solid #d50000; - } -} - - -.content { - background: var(--modal-content-background); - - border: 1px solid black; - border-radius: $border_radius_middle; - - width: max-content; - max-width: 100%; - min-width: 20em; - - min-height: min-content; - max-height: 100%; - - flex-shrink: 1; - flex-grow: 0; /* we dont want a grow over the limit set within the content, but we want to shrink the content if necessary */ - align-self: center; - - overflow: hidden; - - display: flex; - flex-direction: column; - justify-content: stretch; - - .header { - background-color: #222224; - - flex-grow: 0; - flex-shrink: 0; - - display: flex; - flex-direction: row; - justify-content: stretch; - - padding: .25em; - @include user-select(none); - - .icon, .button { - flex-grow: 0; - flex-shrink: 0; - } - - .button { - height: 1.4em; - width: 1.4em; - - padding: .2em; - border-radius: .2em; - - cursor: pointer; - display: flex; - - -webkit-app-region: no-drag; - pointer-events: all; - - &:hover { - background-color: #1b1b1c; - } - } - - .icon { - margin-left: .25em; - margin-right: .5em; - - display: flex; - flex-direction: column; - justify-content: center; - - img { - height: 1.2em; - width: 1.2em; - margin-bottom: .2em; - } - } - - .title, { - flex-grow: 1; - flex-shrink: 1; - - color: #9d9d9e; - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - h5 { - margin: 0; - padding: 0; - } - } - - .body { - max-width: 100%; - min-width: 20em; /* may adjust if needed */ - - min-height: 5em; - - overflow-y: auto; - overflow-x: auto; - - display: flex; - flex-direction: column; - position: relative; - - /* explicitly set the background color so the next element could use background-color: inherited; */ - background: var(--modal-content-background); - - @include chat-scrollbar(); - } -} - -.content { - /* max-height: 500px; */ - min-height: 0; /* required for moz */ - flex-direction: column; - justify-content: stretch; - - .header { - flex-shrink: 0; - flex-grow: 0; - } - - .body { - flex-grow: 1; - flex-shrink: 1; - display: flex; - flex-direction: column; - min-height: 0; - } -} - -.contentInternal { - /* align us in the center */ - margin-right: auto; - margin-left: auto; - - .body { - max-height: calc(100vh - 8em); /* leave some space at the bottom */ - } -} \ No newline at end of file diff --git a/shared/js/ui/react-elements/internal-modal/Renderer.tsx b/shared/js/ui/react-elements/internal-modal/Renderer.tsx deleted file mode 100644 index f5ae22d0..00000000 --- a/shared/js/ui/react-elements/internal-modal/Renderer.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import * as React from "react"; -import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions"; -import {ClientIcon} from "svg-sprites/client-icons"; -import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary"; -import {useMemo} from "react"; - -const cssStyle = require("./Modal.scss"); - -export const InternalModalContentRenderer = React.memo((props: { - modal: AbstractModal, - - onClose?: () => void, - onMinimize?: () => void, - - containerClass?: string, - headerClass?: string, - headerTitleClass?: string, - bodyClass?: string, - - refContent?: React.Ref, - - windowed: boolean, -}) => { - const body = useMemo(() => props.modal.renderBody({ windowed: props.windowed }), [props.modal]); - const title = useMemo(() => props.modal.renderTitle(), [props.modal]); - - return ( -
-
-
- {tr("Modal -
-
- - {title} - -
- {!props.onMinimize ? undefined : ( -
-
-
- )} - {!props.onClose ? undefined : ( -
-
-
- )} -
-
- - {body} - -
-
- ); -}); - -export class InternalModalRenderer extends React.PureComponent<{ modal: AbstractModal, onClose: () => void }, { show: boolean }> { - private readonly refModal = React.createRef(); - - constructor(props) { - super(props); - - this.state = { show: false }; - } - - render() { - let modalExtraClass = ""; - - const type = this.props.modal.type(); - if(typeof type === "string" && type !== "none") { - modalExtraClass = cssStyle["modal-type-" + type]; - } - - const showClass = this.state.show ? cssStyle.shown : ""; - return ( -
this.onBackdropClick(event)} - ref={this.refModal} - > -
- -
-
- ); - } - - private onBackdropClick(event: React.MouseEvent) { - if(event.target !== this.refModal.current || event.isDefaultPrevented()) - return; - - this.props.onClose(); - } -} \ No newline at end of file diff --git a/shared/js/ui/react-elements/modal/Controller.ts b/shared/js/ui/react-elements/modal/Controller.ts new file mode 100644 index 00000000..833a490c --- /dev/null +++ b/shared/js/ui/react-elements/modal/Controller.ts @@ -0,0 +1,134 @@ +import { + AbstractModal, + InternalModal, + ModalConstructorArguments, + ModalController, + ModalEvents, + ModalInstanceController, + ModalOptions, + ModalState +} from "tc-shared/ui/react-elements/modal/Definitions"; +import {Registry} from "tc-events"; +import {findRegisteredModal, RegisteredModal} from "tc-shared/ui/react-elements/modal/Registry"; +import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal"; +import {InternalModalInstance} from "tc-shared/ui/react-elements/modal/internal"; + +export class GenericModalController implements ModalController { + private readonly events: Registry; + + private readonly modalType: T; + private readonly modalConstructorArguments: ModalConstructorArguments[T]; + private readonly modalOptions: ModalOptions; + + private modalKlass: RegisteredModal | undefined; + private instance: ModalInstanceController; + private popedOut: boolean; + + public static fromInternalModal(klass: new (...args: any[]) => ModalClass, constructorArguments: any[]) : GenericModalController<"__internal__modal__"> { + const result = new GenericModalController("__internal__modal__", constructorArguments, { + popoutable: false, + popedOut: false + }); + + result.modalKlass = new class implements RegisteredModal<"__internal__modal__"> { + async classLoader(): Promise<{ default: { new(...args: ModalConstructorArguments["__internal__modal__"]): AbstractModal } }> { + return { default: klass }; + } + + modalId: "__internal__modal__"; + popoutSupported: false; + }; + + return result; + } + + constructor(modalType: T, constructorArguments: ModalConstructorArguments[T], options: ModalOptions) { + this.events = new Registry(); + + this.modalType = modalType; + this.modalConstructorArguments = constructorArguments; + this.modalOptions = options || {}; + + this.popedOut = this.modalOptions.popedOut; + if(typeof this.popedOut !== "boolean") { + this.popedOut = false; + } + } + + private getModalClass() { + const modalKlass = this.modalKlass || findRegisteredModal(this.modalType); + if(!modalKlass) { + throw tr("missing modal registration for") + this.modalType; + } + + return modalKlass; + } + + private createModalInstance() { + if(this.popedOut) { + this.instance = spawnExternalModal(this.modalType, this.modalConstructorArguments, this.modalOptions); + } else { + this.instance = new InternalModalInstance(this.getModalClass(), this.modalConstructorArguments, this.modalOptions); + } + + const events = this.instance.getEvents(); + events.on("notify_destroy", events.on("notify_open", () => this.events.fire("open"))); + events.on("notify_destroy", events.on("notify_close", () => this.events.fire("close"))); + + events.on("action_close", () => this.destroy()); + events.on("action_popout", () => { + if(!this.popedOut) { + if(!this.getModalClass().popoutSupported) { + return; + } + + this.destroyModalInstance(); + this.popedOut = true; + this.show().then(undefined); + } else { + if(this.modalOptions.popedOut) { + /* fixed poped out */ + return; + } + + this.destroyModalInstance(); + this.popedOut = false; + this.show().then(undefined); + } + }); + } + + private destroyModalInstance() { + this.instance?.destroy(); + this.instance = undefined; + } + + destroy() { + this.destroyModalInstance(); + this.events.fire("destroy"); + } + + getEvents(): Registry { + return this.events; + } + + getOptions(): Readonly { + return this.modalOptions; + } + + getState(): ModalState { + return this.instance ? this.instance.getState() : ModalState.DESTROYED; + } + + async hide(): Promise { + await this.instance?.hide(); + } + + async show(): Promise { + if(!this.instance) { + this.createModalInstance(); + } + + await this.instance.show(); + } +} \ No newline at end of file diff --git a/shared/js/ui/react-elements/modal/Definitions.ts b/shared/js/ui/react-elements/modal/Definitions.ts index 9658ed9b..382eaa65 100644 --- a/shared/js/ui/react-elements/modal/Definitions.ts +++ b/shared/js/ui/react-elements/modal/Definitions.ts @@ -4,11 +4,13 @@ import {ChannelEditEvents} from "tc-shared/ui/modal/channel-edit/Definitions"; import {EchoTestEvents} from "tc-shared/ui/modal/echo-test/Definitions"; import {ModalGlobalSettingsEditorEvents} from "tc-shared/ui/modal/global-settings-editor/Definitions"; import {InviteUiEvents, InviteUiVariables} from "tc-shared/ui/modal/invite/Definitions"; - -import {ReactElement} from "react"; -import * as React from "react"; +import React, {ReactElement} from "react"; import {IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable"; import {ModalBookmarkEvents, ModalBookmarkVariables} from "tc-shared/ui/modal/bookmarks/Definitions"; +import { + ModalBookmarksAddServerEvents, + ModalBookmarksAddServerVariables +} from "tc-shared/ui/modal/bookmarks-add-server/Definitions"; export type ModalType = "error" | "warning" | "info" | "none"; export type ModalRenderType = "page" | "dialog"; @@ -52,21 +54,10 @@ export interface ModalOptions { popedOut?: boolean } -export interface ModalFunctionController { - minimize(); - supportMinimize() : boolean; - - maximize(); - supportMaximize() : boolean; - - close(); -} - export interface ModalEvents { "open": {}, "close": {}, - /* create is implicitly at object creation */ "destroy": {} } @@ -76,6 +67,27 @@ export enum ModalState { DESTROYED } +export interface ModalInstanceEvents { + action_close: {}, + action_minimize: {}, + action_popout: {}, + + notify_open: {} + notify_minimize: {}, + notify_close: {}, + notify_destroy: {}, +} + +export interface ModalInstanceController { + getState() : ModalState; + getEvents() : Registry; + + show() : Promise; + hide() : Promise; + + destroy(); +} + export interface ModalController { getOptions() : Readonly; getEvents() : Registry; @@ -87,10 +99,23 @@ export interface ModalController { destroy(); } -export abstract class AbstractModal { - protected constructor() {} +export interface ModalInstanceProperties { + windowed: boolean +} - abstract renderBody(properties: { windowed: boolean }) : ReactElement; +let currentModalProperties: ModalInstanceProperties +export abstract class AbstractModal { + protected readonly properties: ModalInstanceProperties; + + protected constructor() { + if(typeof currentModalProperties === "undefined") { + throw "missing modal properties"; + } + this.properties = currentModalProperties; + currentModalProperties = undefined; + } + + abstract renderBody() : ReactElement; abstract renderTitle() : string | React.ReactElement; /* only valid for the "inline" modals */ @@ -104,13 +129,24 @@ export abstract class AbstractModal { protected onClose() {} protected onOpen() {} } +export abstract class InternalModal extends AbstractModal {} - -export interface ModalRenderer { - renderModal(modal: AbstractModal | undefined); +export function constructAbstractModalClass( + klass: new (...args: ModalConstructorArguments[T]) => AbstractModal, + properties: ModalInstanceProperties, + args: ModalConstructorArguments[T]) : AbstractModal { + currentModalProperties = properties; + try { + return new klass(...args); + } finally { + currentModalProperties = undefined; + } } + export interface ModalConstructorArguments { + "__internal__modal__": any[], + "video-viewer": [ /* events */ IpcRegistryDescription, /* handlerId */ string, @@ -137,5 +173,9 @@ export interface ModalConstructorArguments { "modal-bookmarks": [ /* events */ IpcRegistryDescription, /* variables */ IpcVariableDescriptor, + ], + "modal-bookmark-add-server": [ + /* events */ IpcRegistryDescription, + /* variables */ IpcVariableDescriptor, ] } \ No newline at end of file diff --git a/shared/js/ui/react-elements/modal/Registry.ts b/shared/js/ui/react-elements/modal/Registry.ts index 1f442669..fc1ebd84 100644 --- a/shared/js/ui/react-elements/modal/Registry.ts +++ b/shared/js/ui/react-elements/modal/Registry.ts @@ -79,3 +79,9 @@ registerModal({ popoutSupported: true }); +registerModal({ + modalId: "modal-bookmark-add-server", + classLoader: async () => await import("tc-shared/ui/modal/bookmarks-add-server/Renderer"), + popoutSupported: true +}); + diff --git a/shared/js/ui/react-elements/modal/Renderer.scss b/shared/js/ui/react-elements/modal/Renderer.scss new file mode 100644 index 00000000..2dfd679c --- /dev/null +++ b/shared/js/ui/react-elements/modal/Renderer.scss @@ -0,0 +1,223 @@ +@import "../../../../css/static/mixin"; +@import "../../../../css/static/properties"; + +html:root { + --modal-content-background: #19191b; + + --modal-color-blue: #0a73d2; + --modal-color-green: #00d400; + --modal-color-red: #d50000; +} + +.modalTitle { + background-color: #222224; + + flex-grow: 0; + flex-shrink: 0; + + display: flex; + flex-direction: row; + justify-content: stretch; + + padding: .25em; + @include user-select(none); + + .icon, .button { + flex-grow: 0; + flex-shrink: 0; + } + + .button { + height: 1.4em; + width: 1.4em; + + padding: .2em; + border-radius: .2em; + + cursor: pointer; + display: flex; + + -webkit-app-region: no-drag; + pointer-events: all; + + &:hover { + background-color: #1b1b1c; + } + + &:not(:last-of-type) { + margin-right: .25em; + } + } + + .icon { + margin-left: .25em; + margin-right: .5em; + + display: flex; + flex-direction: column; + justify-content: center; + + img { + height: 1.2em; + width: 1.2em; + margin-bottom: .2em; + } + } + + .title, { + flex-grow: 1; + flex-shrink: 1; + + color: #9d9d9e; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + h5 { + margin: 0; + padding: 0; + } +} + +.modalBody { + position: relative; + + min-width: 10em; + min-height: 5em; + + overflow-y: auto; + overflow-x: auto; + + display: flex; + flex-direction: column; + + /* explicitly set the background color so the next element could use background-color: inherited; */ + background: var(--modal-content-background); + + @include chat-scrollbar(); + + &.color-blue { + border-left: 2px solid var(--modal-color-blue); + } + + &.color-green { + border-left: 2px solid var(--modal-color-green); + } + + &.color-red { + border-left: 2px solid var(--modal-color-red); + } +} + +.modalFrame { + background: var(--modal-content-background); + + border: 1px solid black; + border-radius: $border_radius_middle; + + width: max-content; + max-width: 100%; + min-width: 20em; + + min-height: min-content; + max-height: 100%; + + flex-shrink: 1; + flex-grow: 0; /* we dont want a grow over the limit set within the content, but we want to shrink the content if necessary */ + + margin-left: auto; + margin-right: auto; + + overflow: hidden; + + display: flex; + flex-direction: column; + justify-content: stretch; +} + +.modalWindowContainer { + position: absolute; + + top: 0; + left: 0; + right: 0; + bottom: 0; + + display: flex; + flex-direction: column; + justify-content: stretch; + + max-height: 100vh; + max-width: 100vw; + + .modalTitle { + display: none!important; + } + + .modalBody { + position: absolute; + + top: 0; + left: 0; + right: 0; + bottom: 0; + } +} + +.modalPageContainer { + color: var(--text); /* base color */ + + overflow: auto; /* allow scrolling if a modal is too big */ + + background-color: rgba(0, 0, 0, 0.8); + + padding-right: 5%; + padding-left: 5%; + + z-index: 100000; + position: fixed; + + top: 0; + left: 0; + right: 0; + bottom: 0; + + display: flex; + flex-direction: column; + + opacity: 0; + margin-top: -1000vh; + + $animation_length: .3s; + @include transition(opacity $animation_length ease-in, margin-top $animation_length ease-in); + &.shown { + margin-top: 0; + opacity: 1; + } + + &.align-top { + justify-content: flex-start; + } + + &.align-center { + justify-content: center; + } + + &.align-bottom { + justify-content: flex-end; + } + + .dialog { + display: block; + + margin: 1.75rem 0; + + /* width calculations */ + align-items: center; + + /* height stuff */ + max-height: calc(100% - 3.5em); + } +} \ No newline at end of file diff --git a/shared/js/ui/react-elements/modal/Renderer.tsx b/shared/js/ui/react-elements/modal/Renderer.tsx index c963674f..f9cb7b2d 100644 --- a/shared/js/ui/react-elements/modal/Renderer.tsx +++ b/shared/js/ui/react-elements/modal/Renderer.tsx @@ -1,43 +1,189 @@ -import { - AbstractModal, - ModalFunctionController, - ModalOptions, - ModalRenderType -} from "tc-shared/ui/react-elements/modal/Definitions"; -import {useContext} from "react"; +import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions"; +import React from "react"; +import {joinClassList} from "tc-shared/ui/react-elements/Helper"; +import TeaCupImage from "./TeaCup.png"; +import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary"; +import {ClientIcon} from "svg-sprites/client-icons"; +import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons"; -const ControllerContext = useContext(undefined); +const cssStyle = require("./Renderer.scss"); -interface RendererControllerEvents { +export class ModalFrameTopRenderer extends React.PureComponent<{ + modalInstance: AbstractModal, -} + className?: string, -export class ModalRendererController { - readonly renderType: ModalRenderType; - readonly modal: AbstractModal; + onClose?: () => void, + onPopout?: () => void, + onMinimize?: () => void, - constructor(renderType: ModalRenderType, modal: AbstractModal,) { - this.renderType = renderType; - this.modal = modal; + replacePageTitle: boolean, +}> { + private readonly refTitle = React.createRef(); + private titleElement: HTMLTitleElement; + private observer: MutationObserver; + + componentDidMount() { + if(this.props.replacePageTitle) { + const titleElements = document.getElementsByTagName("title"); + if(titleElements.length === 0) { + this.titleElement = document.createElement("title"); + document.head.appendChild(this.titleElement); + } else { + this.titleElement = titleElements[0]; + } + + this.observer = new MutationObserver(() => this.updatePageTitle()); + this.observer.observe(this.refTitle.current, { + attributes: true, + subtree: true, + childList: true, + characterData: true + }); + this.updatePageTitle(); + } } - setShown(shown: boolean) { + componentWillUnmount() { + this.observer?.disconnect(); + this.observer = undefined; + this.titleElement = undefined; + } + render() { + const buttons = []; + if(this.props.onMinimize) { + buttons.push( +
+ +
+ ); + } + + if(this.props.onPopout) { + buttons.push( +
+ +
+ ); + } + + if(this.props.onClose) { + buttons.push( +
+ +
+ ); + } + + return ( +
+
+ {""} +
+
+ + {this.props.modalInstance.renderTitle()} + +
+ {buttons} +
+ ); + } + + private updatePageTitle() { + if(!this.refTitle.current) { + return; + } + + this.titleElement.innerText = this.refTitle.current.textContent; } } -export const ModalRenderer = (props: { - mode: "page" | "dialog", - modal: AbstractModal, - modalOptions: ModalOptions, - modalActions: ModalFunctionController -}) => { +export class ModalBodyRenderer extends React.PureComponent<{ + modalInstance: AbstractModal, + className?: string +}> { + constructor(props) { + super(props); + } + + render() { + return ( +
+ {this.props.modalInstance.renderBody()} +
+ ) + } } -const ModalRendererDialog = (props: { - modal: AbstractModal, - modalOptions: ModalOptions, - modalActions: ModalFunctionController +export class ModalFrameRenderer extends React.PureComponent<{ + children: [React.ReactElement, React.ReactElement] +}> { + render() { + return ( +
+ {this.props.children[0]} + {this.props.children[1]} +
+ ); + } +} + +export class PageModalRenderer extends React.PureComponent<{ + modalInstance: AbstractModal, + onBackdropClicked: () => void, + children: React.ReactElement +}, { + shown: boolean +}> { + constructor(props) { + super(props); + + this.state = { + shown: false + }; + } + + render() { + return ( +
{ + if(event.target !== event.currentTarget) { + return; + } + + this.props.onBackdropClicked(); + }} + > +
+ {this.props.children} +
+
+ ); + } +} + +export const WindowModalRenderer = (props: { + children: [React.ReactElement, React.ReactElement] }) => { + return ( +
+ {props.children[0]} + {props.children[1]} +
+ ); } \ No newline at end of file diff --git a/shared/js/ui/react-elements/modal/TeaCup.png b/shared/js/ui/react-elements/modal/TeaCup.png new file mode 100644 index 0000000000000000000000000000000000000000..3eda87e537836604e1d144160a9c5ea0166b7749 GIT binary patch literal 65631 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

*M436!11O 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: T, constructorArguments: ModalConstructorArguments[T], options?: ModalOptions) : ModalController { - if(options?.popedOut) { - return spawnExternalModal(modal, constructorArguments, options); - } else { - return spawnInternalModal(modal, constructorArguments, options); - } + return new GenericModalController(modal, constructorArguments, options); } -export function spawnReactModal(modalClass: new () => ModalClass) : InternalModalController; -export function spawnReactModal(modalClass: new (..._: [A1]) => ModalClass, arg1: A1) : InternalModalController; -export function spawnReactModal(modalClass: new (..._: [A1, A2]) => ModalClass, arg1: A1, arg2: A2) : InternalModalController; -export function spawnReactModal(modalClass: new (..._: [A1, A2, A3]) => ModalClass, arg1: A1, arg2: A2, arg3: A3) : InternalModalController; -export function spawnReactModal(modalClass: new (..._: [A1, A2, A3, A4]) => ModalClass, arg1: A1, arg2: A2, arg3: A3, arg4: A4) : InternalModalController; -export function spawnReactModal(modalClass: new (..._: [A1, A2, A3, A4]) => ModalClass, arg1: A1, arg2: A2, arg3: A3, arg4: A4, arg5: A5) : InternalModalController; -export function spawnReactModal(modalClass: new (..._: any[]) => ModalClass, ...args: any[]) : InternalModalController { - return new InternalModalController({ - popoutSupported: false, - modalId: "__internal__unregistered", - classLoader: async () => ({ default: modalClass }) - }, args); -} - -export function spawnInternalModal(modal: T, constructorArguments: ModalConstructorArguments[T], options?: ModalOptions) : InternalModalController { - return new InternalModalController(findRegisteredModal(modal), constructorArguments); +export function spawnReactModal(modalClass: new () => ModalClass) : ModalController; +export function spawnReactModal(modalClass: new (..._: [A1]) => ModalClass, arg1: A1) : ModalController; +export function spawnReactModal(modalClass: new (..._: [A1, A2]) => ModalClass, arg1: A1, arg2: A2) : ModalController; +export function spawnReactModal(modalClass: new (..._: [A1, A2, A3]) => ModalClass, arg1: A1, arg2: A2, arg3: A3) : ModalController; +export function spawnReactModal(modalClass: new (..._: [A1, A2, A3, A4]) => ModalClass, arg1: A1, arg2: A2, arg3: A3, arg4: A4) : ModalController; +export function spawnReactModal(modalClass: new (..._: [A1, A2, A3, A4]) => ModalClass, arg1: A1, arg2: A2, arg3: A3, arg4: A4, arg5: A5) : ModalController; +export function spawnReactModal(modalClass: new (..._: any[]) => ModalClass, ...args: any[]) : ModalController { + return GenericModalController.fromInternalModal(modalClass, args); } \ No newline at end of file diff --git a/shared/js/ui/react-elements/modal/internal/index.tsx b/shared/js/ui/react-elements/modal/internal/index.tsx new file mode 100644 index 00000000..26e67af7 --- /dev/null +++ b/shared/js/ui/react-elements/modal/internal/index.tsx @@ -0,0 +1,172 @@ +import { + AbstractModal, + 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, + ModalFrameTopRenderer, + PageModalRenderer +} from "tc-shared/ui/react-elements/modal/Renderer"; +import {RegisteredModal} from "tc-shared/ui/react-elements/modal/Registry"; +import {LogCategory, logError} from "tc-shared/log"; +import {Registry} from "tc-events"; + +export class InternalModalInstance implements ModalInstanceController { + readonly events: Registry; + + 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; + + private modalInitializePromise: Promise; + + constructor(modalType: RegisteredModal, constructorArguments: any[], modalOptions: ModalOptions) { + this.events = new Registry(); + + this.modalKlass = modalType; + this.modalOptions = modalOptions; + this.constructorArguments = constructorArguments; + + this.rendererInstance = React.createRef(); + this.state = ModalState.DESTROYED; + } + + private async constructModal() { + if(this.htmlContainer || this.modalInstance) { + throw tr("internal modal has already been constructed"); + } + + const modalClass = await this.modalKlass.classLoader(); + if(!modalClass) { + throw tr("invalid modal class"); + } + + try { + this.modalInstance = constructAbstractModalClass(modalClass.default, { windowed: false }, this.constructorArguments); + } catch (error) { + 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); + + await new Promise(resolve => { + ReactDOM.render( + + + + + + , + this.htmlContainer, + resolve + ); + }); + } + + private destructModal() { + this.state = ModalState.DESTROYED; + if(this.htmlContainer) { + ReactDOM.unmountComponentAtNode(this.htmlContainer); + this.htmlContainer.remove(); + this.htmlContainer = undefined; + } + + if(this.modalInstance) { + this.modalInstance["onDestroy"](); + this.modalInstance = undefined; + } + this.events.fire("notify_destroy"); + } + + getState(): ModalState { + return this.state; + } + + getEvents(): Registry { + return this.events; + } + + async show() : Promise { + if(!this.modalInstance) { + this.modalInitializePromise = this.constructModal(); + } + + if(this.modalInitializePromise) { + await this.modalInitializePromise; + } + + if(!this.rendererInstance.current) { + return; + } + + this.state = ModalState.SHOWN; + this.modalInstance["onOpen"](); + await new Promise(resolve => this.rendererInstance.current.setState({ shown: true }, resolve)); + this.events.fire("notify_open"); + } + + async hide() : Promise { + if(this.modalInitializePromise) { + await this.modalInitializePromise; + } + + if(!this.rendererInstance.current) { + return; + } + + this.state = ModalState.HIDDEN; + this.modalInstance["onClose"](); + await new Promise(resolve => this.rendererInstance.current.setState({ shown: false }, resolve)); + + /* TODO: Somehow get the real animation finish signal? */ + await new Promise(resolve => setTimeout(resolve, 500)); + this.events.fire("notify_close"); + } + + destroy() { + this.destructModal(); + this.events.destroy(); + } + + private getCloseCallback() { + return () => this.events.fire("action_close"); + } + + private getPopoutCallback() { + if(!this.modalKlass.popoutSupported) { + return undefined; + } + + if(typeof this.modalOptions.popoutable !== "boolean" || !this.modalOptions.popoutable) { + return undefined; + } + + return () => this.events.fire("action_popout"); + } + + private getMinimizeCallback() { + /* We can't minimize any windows */ + return undefined; + } +} \ No newline at end of file diff --git a/shared/js/ui/tree/EntryTags.scss b/shared/js/ui/tree/EntryTags.scss index c739350f..97857c55 100644 --- a/shared/js/ui/tree/EntryTags.scss +++ b/shared/js/ui/tree/EntryTags.scss @@ -1,4 +1,4 @@ -.client { +.tag { display: inline-block; color: #D8D8D8; font-weight: 700; diff --git a/shared/js/ui/tree/EntryTags.tsx b/shared/js/ui/tree/EntryTags.tsx index 68036eb0..e99c6452 100644 --- a/shared/js/ui/tree/EntryTags.tsx +++ b/shared/js/ui/tree/EntryTags.tsx @@ -11,8 +11,42 @@ const cssStyle = require("./EntryTags.scss"); let ipcChannel: IPCChannel; -export const ClientTag = (props: { clientName: string, clientUniqueId: string, handlerId: string, clientId?: number, clientDatabaseId?: number, className?: string }) => ( -

{ + return ( +
{ + event.preventDefault(); + + ipcChannel.sendMessage("contextmenu-server", { + handlerId: props.handlerId, + serverUniqueId: props.serverUniqueId, + + pageX: event.pageX, + pageY: event.pageY + }); + }} + draggable={false} + > + {props.serverName} +
+ ) +}); + +export const ClientTag = React.memo((props: { + clientName: string, + clientUniqueId: string, + handlerId: string, + clientId?: number, + clientDatabaseId?: number, + className?: string +}) => ( +
{ event.preventDefault(); @@ -45,11 +79,16 @@ export const ClientTag = (props: { clientName: string, clientUniqueId: string, h > {props.clientName}
-); +)); -export const ChannelTag = (props: { channelName: string, channelId: number, handlerId: string, className?: string }) => ( +export const ChannelTag = React.memo((props: { + channelName: string, + channelId: number, + handlerId: string, + className?: string +}) => (
{ event.preventDefault(); @@ -77,8 +116,7 @@ export const ChannelTag = (props: { channelName: string, channelId: number, hand > {props.channelName}
-); - +)); loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { name: "entry tags", diff --git a/web/app/legacy/audio-lib/index.ts b/web/app/legacy/audio-lib/index.ts index dc83b5b2..9ae5a2fe 100644 --- a/web/app/legacy/audio-lib/index.ts +++ b/web/app/legacy/audio-lib/index.ts @@ -22,7 +22,7 @@ export class AudioLibrary { } private static spawnNewWorker() : Worker { - /* + /* * Attention don't use () => new Worker(...). * This confuses the worker plugin and will not emit any modules */